Fix learn dialog rendering, add lab publish/draft toggle, bump to v0.1.2
- learn/index.html: restructured dialogs inside section divs to fix scope - learn/index.html: edit-article dialogs via flat articles list in modal blocks - LearnController: sectionChoices for select options; secure article fetching - ContentService: createArticlePlaced: new section adjacent to anchor section - lab status: published = student-visible, draft = hidden - lab toggle: click badge in labs list or button in lab header to switch status - added badge-btn CSS class, removed inline styles - removed all th:onclick with expressions → data-modal attributes - footer v0.1.2
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
|
||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-new-article')">+ Статья</button>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import')">Импорт zip</button>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-manage')">Главы и разделы</button>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-new-chapter')">+ Глава</button>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${notice}" class="alert alert-success" th:text="${notice}"></div>
|
||||
@@ -17,31 +17,34 @@
|
||||
<div th:if="${tree == null || #lists.isEmpty(tree.chapters)}" class="card card-body text-muted">
|
||||
Материалы ещё не добавлены.
|
||||
</div>
|
||||
<div th:each="chapter : ${tree.chapters}" class="card mb-4" th:id="|chapter-${chapter.id}|">
|
||||
|
||||
<div th:each="chapter : ${tree?.chapters}" class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between gap-4 mb-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||
<h2 class="section-title" th:text="${chapter.title}">Глава</h2>
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-rename-chapter-'+${chapter.id})">Переименовать</button>
|
||||
th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="openModalData(this)">Переименовать</button>
|
||||
<form th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить главу со всеми разделами и статьями?')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-danger btn-sm" type="submit">Удалить</button>
|
||||
<button class="btn btn-danger btn-sm" type="submit">Удалить главу</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div th:each="section : ${chapter.sections}" class="mt-4"
|
||||
th:id="|section-${section.id}|">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
|
||||
<p th:if="${#lists.isEmpty(chapter.sections)}" class="text-sm text-muted">Разделов пока нет</p>
|
||||
|
||||
<div th:each="section : ${chapter.sections}" class="mt-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h3 class="group-title">
|
||||
<a th:href="@{/learn/{c}/{s}(c=${chapter.slug}, s=${section.slug})}" th:text="${section.title}">Раздел</a>
|
||||
</h3>
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex gap-2">
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-rename-section-'+${section.id})">Переименовать</button>
|
||||
th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)">Переименовать</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-move-'+${section.id})">Перенести статьи</button>
|
||||
th:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)">Перенести статьи</button>
|
||||
<form th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить раздел со всеми статьями?')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
@@ -55,7 +58,7 @@
|
||||
th:text="${article.title}">Статья</a>
|
||||
<th:block th:if="${currentUser != null and currentUser.admin}">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-edit-article-'+${article.id})">Править</button>
|
||||
th:data-modal="|modal-edit-article-${article.id}|" onclick="openModalData(this)">Править</button>
|
||||
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить статью?')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
@@ -65,131 +68,141 @@
|
||||
</li>
|
||||
<li th:if="${#lists.isEmpty(section.articles)}" class="text-muted">Статей пока нет</li>
|
||||
</ul>
|
||||
|
||||
<th:block th:if="${currentUser != null and currentUser.admin}">
|
||||
<dialog th:id="|modal-rename-section-${section.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать раздел</h2>
|
||||
<button class="modal-close" type="button"
|
||||
th:data-modal="|modal-rename-section-${section.id}|" onclick="closeModalData(this)">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="title" th:value="${section.title}" required></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog th:id="|modal-move-${section.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||
<button class="modal-close" type="button"
|
||||
th:data-modal="|modal-move-${section.id}|" onclick="closeModalData(this)">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<p class="text-sm text-muted">Выберите раздел-приёмник: статьи перенесутся туда, а текущий раздел будет удалён.</p>
|
||||
<div><label class="label">Перенести в раздел</label>
|
||||
<select class="input" name="moveTo">
|
||||
<option th:each="pair : ${sectionChoices}"
|
||||
th:value="${pair['id']}" th:text="${pair['label']}">—</option>
|
||||
</select></div>
|
||||
<button class="btn btn-primary" type="submit">Перенести и удалить раздел</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
<p th:if="${#lists.isEmpty(chapter.sections)}" class="text-sm text-muted">Разделов пока нет</p>
|
||||
|
||||
<th:block th:if="${currentUser != null and currentUser.admin}">
|
||||
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать главу</h2>
|
||||
<button class="modal-close" type="button"
|
||||
th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="closeModalData(this)">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="title" th:value="${chapter.title}" required></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="modal-import" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Импорт материалов (zip)</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-import')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<p class="text-sm text-muted">
|
||||
Структура: <code class="code">chapters/01-глава/01-раздел/01-статья.md</code>, картинки в <code class="code">assets/</code>.
|
||||
</p>
|
||||
<div><label class="label" for="file">Архив</label>
|
||||
<input class="input" id="file" name="file" type="file" accept=".zip" required></div>
|
||||
<button class="btn btn-primary" type="submit">Импортировать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-new-article" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Новая статья</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-new-article')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="naChapter">Глава (можно создать новую)</label>
|
||||
<div class="inline-form w-full">
|
||||
<select class="input" id="naChapter" name="chapterId" th:if="${tree != null}">
|
||||
<option th:each="chapter : ${tree.chapters}" th:value="${chapter.id}" th:text="${chapter.title}">—</option>
|
||||
</select>
|
||||
<span class="text-muted">или</span>
|
||||
<input class="input" name="newChapterTitle" placeholder="Новая глава">
|
||||
</div>
|
||||
</div>
|
||||
<div><label class="label" for="naSection">Раздел (можно создать новый)</label>
|
||||
<input class="input" id="naSection" name="newSectionTitle" placeholder="Новый раздел"></div>
|
||||
<div><label class="label" for="naTitle">Заголовок статьи</label>
|
||||
<input class="input" id="naTitle" name="title" required></div>
|
||||
<div><label class="label" for="naBody">Текст (markdown)</label>
|
||||
<textarea class="input" id="naBody" name="bodyMd" rows="10"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<th:block th:each="chapter : ${tree?.chapters ?: {}}">
|
||||
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal">
|
||||
<dialog id="modal-import" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать главу</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-rename-chapter-'+[[${chapter.id}]])">✕</button>
|
||||
<h2 class="modal-title">Импорт материалов (zip)</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-import')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
||||
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="title" th:value="${chapter.title}" required></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<p class="text-sm text-muted">
|
||||
Структура: <code class="code">chapters/01-глава/01-раздел/01-статья.md</code>, картинки в <code class="code">assets/</code>.
|
||||
</p>
|
||||
<div><label class="label" for="file">Архив</label>
|
||||
<input class="input" id="file" name="file" type="file" accept=".zip" required></div>
|
||||
<button class="btn btn-primary" type="submit">Импортировать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<th:block th:each="section : ${chapter.sections}">
|
||||
<dialog th:id="|modal-rename-section-${section.id}|" class="modal">
|
||||
|
||||
<dialog id="modal-new-chapter" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Новая глава</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-new-chapter')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="chTitle">Название</label>
|
||||
<input class="input" id="chTitle" name="title" required></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-new-article" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Новая статья</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-new-article')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="naChapter">Существующая глава и раздел</label>
|
||||
<select class="input" id="naChapter" name="sectionId">
|
||||
<option th:each="pair : ${sectionChoices}"
|
||||
th:value="${pair['id']}" th:text="${pair['label']}">—</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="naNewSection">…или новый раздел в выбранной главе</label>
|
||||
<input class="input" id="naNewSection" name="newSectionTitle" placeholder="Новый раздел"></div>
|
||||
<div><label class="label" for="naTitle">Заголовок статьи</label>
|
||||
<input class="input" id="naTitle" name="title" required></div>
|
||||
<div><label class="label" for="naBody">Текст (markdown)</label>
|
||||
<textarea class="input" id="naBody" name="bodyMd" rows="10"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<th:block th:if="${currentUser != null and currentUser.admin}" th:each="a : ${articles}">
|
||||
<dialog th:id="|modal-edit-article-${a.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать раздел</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-rename-section-'+[[${section.id}]])">✕</button>
|
||||
<h2 class="modal-title">Правка статьи</h2>
|
||||
<button class="modal-close" type="button"
|
||||
th:data-modal="|modal-edit-article-${a.id}|" onclick="closeModalData(this)">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||
<form class="form-stack" th:action="@{/admin/content/articles/{id}/save(id=${a.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="title" th:value="${section.title}" required></div>
|
||||
<div><label class="label">Заголовок</label>
|
||||
<input class="input" name="title" th:value="${a.title}" required></div>
|
||||
<div><label class="label">Текст (markdown)</label>
|
||||
<textarea class="input" name="bodyMd" rows="14" th:text="${a.bodyMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog th:id="|modal-move-${section.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-move-'+[[${section.id}]])">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<p class="text-sm text-muted">Укажите раздел, в который перенести статьи, затем подтвердите удаление раздела.</p>
|
||||
<div><label class="label">Перенести в раздел</label>
|
||||
<select class="input" name="moveTo" th:if="${tree != null}">
|
||||
<th:block th:each="ch2 : ${tree.chapters}">
|
||||
<optgroup th:label="${ch2.title}">
|
||||
<option th:each="s2 : ${ch2.sections}"
|
||||
th:if="${s2.id != section.id}"
|
||||
th:value="${s2.id}" th:text="${ch2.title} / ${s2.title}">—</option>
|
||||
</optgroup>
|
||||
</th:block>
|
||||
</select></div>
|
||||
<button class="btn btn-primary" type="submit">Перенести и удалить раздел</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
<th:block th:each="a : ${articles}">
|
||||
<dialog th:id="|modal-edit-article-${a.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Правка статьи</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-edit-article-'+[[${a.id}]])">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/articles/{id}/save(id=${a.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label">Заголовок</label>
|
||||
<input class="input" name="title" th:value="${a.title}" required></div>
|
||||
<div><label class="label">Текст (markdown)</label>
|
||||
<textarea class="input" name="bodyMd" rows="14" th:text="${a.bodyMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user