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:
2026-09-11 20:41:11 +03:00
parent 006951adae
commit caa48d3193
11 changed files with 219 additions and 131 deletions

View File

@@ -213,6 +213,20 @@ class AdminController(
}
}
@PostMapping("/labs/{id}/toggle-status")
fun toggleStatus(
@PathVariable id: String,
session: HttpSession,
redirect: RedirectAttributes,
): String = try {
api.toggleLabStatus(token(session), id)
redirect.addFlashAttribute("notice", "Статус лабораторной изменён")
"redirect:/admin/labs/$id"
} catch (ex: ApiException) {
redirect.addFlashAttribute("error", ex.message)
"redirect:/admin/labs/$id"
}
@PostMapping("/labs/{id}/delete")
fun deleteLab(
@PathVariable id: String,

View File

@@ -181,6 +181,14 @@ class ApiClient(
}
}
fun toggleLabStatus(token: String, labId: String) {
call {
client.post().uri("/api/v1/labs/{id}/toggle-status", labId)
.header(HttpHeaders.AUTHORIZATION, bearer(token))
.retrieve().toBodilessEntity()
}
}
fun getLevels(token: String, labId: String): List<LevelAssignmentResultDto> {
val type = object : ParameterizedTypeReference<List<LevelAssignmentResultDto>>() {}
return call {

View File

@@ -22,7 +22,24 @@ class LearnController(
@GetMapping("/learn")
fun index(session: HttpSession, model: Model): String {
model.addAttribute("tree", api.contentTree(token(session)))
val t = token(session)
val tree = api.contentTree(t)
model.addAttribute("tree", tree)
val isAdmin = (session.getAttribute(SessionKeys.USER) as? ru.ysu.lms.web.session.SessionUser)?.isAdmin == true
val articles = if (isAdmin && t != null) api.contentTreeList(t) else emptyList()
model.addAttribute("articles", articles)
model.addAttribute(
"sectionChoices",
tree.chapters.flatMap { chapter ->
chapter.sections.map { section ->
mapOf(
"id" to section.id,
"chapterId" to chapter.id,
"label" to "${chapter.title} / ${section.title}",
)
}
},
)
return "learn/index"
}

View File

@@ -136,6 +136,7 @@ a:hover { color: var(--c-text-link-hover); }
.badge { display: inline-flex; align-items: center; border-radius: 9999px; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 500; }
.badge-ok { background: var(--c-badge-ok-bg); color: var(--c-badge-ok-text); }
.badge-warn { background: var(--c-badge-warn-bg); color: var(--c-badge-warn-text); }
.badge-btn { border: none; cursor: pointer; padding: 0.125rem 0.625rem; }
/* Tables */
table { width: 100%; font-size: 0.875rem; border-collapse: collapse; }

View File

@@ -9,6 +9,10 @@
<h1 class="page-title" th:text="|ЛР ${lab.number}. ${lab.title}|">ЛР 1</h1>
<p class="text-sm text-muted">
Статус: <span class="badge" th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'" th:text="${lab.status}">—</span>
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="btn btn-secondary btn-sm ml-2" onclick="return confirm('Сменить статус лабораторной?')" type="submit">Сменить статус</button>
</form>
<span th:if="${lab.deadlineAt != null}"> · Срок: <span th:text="${lab.deadlineAt}"></span></span>
<span th:if="${lab.defenseAt != null}"> · Защита: <span th:text="${lab.defenseAt}"></span></span>
· Максимум: <span th:text="${lab.pointsMax}"></span> б.
@@ -47,14 +51,14 @@
</h3>
<div class="flex gap-2">
<button class="btn btn-secondary btn-sm" type="button"
th:onclick="openModal('modal-rename-level-'+${level.id})">Переименовать</button>
th:attr="data-modal=${'modal-rename-level-' + level.id}" onclick="openModalData(this)">Переименовать</button>
<form th:action="@{/admin/labs/{labId}/levels/{lid}/delete(labId=${lab.id}, lid=${level.id})}" method="post"
onsubmit="return confirm('Удалить уровень без вариантов?')">
<button class="btn btn-danger btn-sm" type="submit">Удалить</button>
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
<button class="btn btn-primary btn-sm" type="button"
th:onclick="openModal('modal-add-variant-'+${level.id})">+ Вариант</button>
th:attr="data-modal=${'modal-add-variant-' + level.id}" onclick="openModalData(this)">+ Вариант</button>
</div>
</div>
<table>
@@ -66,7 +70,7 @@
<td class="text-muted" th:text="${#strings.abbreviate(v.statementMd, 60)}">—</td>
<td class="text-center">
<button class="btn btn-secondary btn-sm" type="button"
th:onclick="openModal('modal-edit-variant-'+${v.id})">Править</button>
th:attr="data-modal=${'modal-edit-variant-' + v.id}" onclick="openModalData(this)">Править</button>
<form class="inline-form" th:action="@{/admin/labs/{labId}/variants/{vid}/delete(labId=${lab.id}, vid=${v.id})}" method="post"
onsubmit="return confirm('Удалить вариант?')">
<button class="btn btn-danger btn-sm" type="submit">Удалить</button>
@@ -189,7 +193,7 @@
<dialog th:each="level : ${lab.levels}" th:id="|modal-rename-level-${level.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Переименовать уровень</h2>
<button class="modal-close" type="button" th:onclick="closeModal('modal-rename-level-'+[[${level.id}]])">✕</button>
<button class="modal-close" type="button" th:attr="data-modal=${'modal-rename-level-' + level.id}" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
@@ -206,7 +210,7 @@
<dialog th:each="level : ${lab.levels}" th:id="|modal-add-variant-${level.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Новый вариант — <span th:text="${level.title}">уровень</span></h2>
<button class="modal-close" type="button" th:onclick="closeModal('modal-add-variant-'+[[${level.id}]])">✕</button>
<button class="modal-close" type="button" th:attr="data-modal=${'modal-add-variant-' + level.id}" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">
@@ -227,7 +231,7 @@
<dialog th:each="v : ${level.variants}" th:id="|modal-edit-variant-${v.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Правка варианта</h2>
<button class="modal-close" type="button" th:onclick="closeModal('modal-edit-variant-'+[[${v.id}]])">✕</button>
<button class="modal-close" type="button" th:attr="data-modal=${'modal-edit-variant-' + v.id}" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">

View File

@@ -7,6 +7,8 @@
<h1 class="page-title">Лабораторные</h1>
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-lab')">+ Создать</button>
</div>
<p class="text-sm text-muted mb-4">Статус <b>draft</b> — черновик (редактирование, студентам не видна).
<b>published</b> — опубликована (доступна студентам, видны распределённые варианты). Нажмите на бейдж статуса, чтобы переключить.</p>
<div th:if="${notice}" class="alert alert-success" th:text="${notice}"></div>
<div th:if="${error}" class="alert alert-error" th:text="${error}"></div>
@@ -18,7 +20,14 @@
<td th:text="${lab.number}">1</td>
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="${lab.title}">—</a></td>
<td th:text="${lab.variantCount}">0</td>
<td><span class="badge" th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'" th:text="${lab.status}">—</span></td>
<td>
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<button class="badge badge-btn"
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
type="submit" th:text="${lab.status}">—</button>
</form>
</td>
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post"><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger btn-sm" type="submit">Удалить</button></form></td>
</tr>
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="5" class="empty">Лабораторных пока нет.</td></tr>

View File

@@ -12,6 +12,8 @@
function toggleTheme(){ var e=document.documentElement; var t=e.getAttribute('data-theme')==='dark'?'light':'dark'; e.setAttribute('data-theme',t); localStorage.setItem('lms-theme',t); }
function openModal(id){ var d=document.getElementById(id); if(d) d.showModal(); }
function closeModal(id){ var d=document.getElementById(id); if(d) d.close(); }
function openModalData(btn){ openModal(btn.dataset.modal); }
function closeModalData(btn){ closeModal(btn.dataset.modal); }
</script>
</head>
<body>
@@ -38,7 +40,7 @@
<th:block th:replace="${content}" />
</main>
<footer class="page-footer">
LMS v0.1.1
LMS v0.1.2
</footer>
</div>
</body>

View File

@@ -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>