Content: drag & drop reordering and moving of chapters, sections and articles
- academic: POST /api/v1/content/reorder applies a full tree arrangement in one request — chapter order, section order and section->chapter moves, article order and article->section moves (positions and parent ids updated) - web: POST /admin/content/reorder accepts the tree from JS (CSRF via query param), calls the gateway with the admin token - /learn admin view: draggable handles (⠿) for chapters, sections and articles; drop before/after to reorder, drop into a chapter/section to move; visual indicators; structure is collected from DOM and saved, then page reloads - docs: class diagram method + new sequence diagram for drag & drop
This commit is contained in:
@@ -61,6 +61,16 @@ data class ContentImportResult(
|
||||
val assets: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Новое расположение материалов: порядок глав, порядок разделов внутри глав
|
||||
* (и перенос разделов между главами), порядок статей внутри разделов
|
||||
* (и перенос статей между разделами). Применяется одним запросом.
|
||||
*/
|
||||
data class ReorderArticle(val id: String)
|
||||
data class ReorderSection(val id: String, val articles: List<ReorderArticle> = emptyList())
|
||||
data class ReorderChapter(val id: String, val sections: List<ReorderSection> = emptyList())
|
||||
data class ReorderTree(val chapters: List<ReorderChapter> = emptyList())
|
||||
|
||||
private data class ParsedArticle(
|
||||
val path: String,
|
||||
val chapterPos: Int,
|
||||
@@ -145,6 +155,36 @@ class ContentService(
|
||||
)
|
||||
}
|
||||
|
||||
/** Применяет новое расположение глав, разделов и статей (drag & drop в админке). */
|
||||
@Transactional
|
||||
fun reorder(tree: ReorderTree) {
|
||||
tree.chapters.forEachIndexed { chapterIndex, chapterDto ->
|
||||
val chapter = chapters.findById(uuidOrNull(chapterDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
chapter.position = chapterIndex + 1
|
||||
chapters.save(chapter)
|
||||
|
||||
chapterDto.sections.forEachIndexed { sectionIndex, sectionDto ->
|
||||
val section = sections.findById(uuidOrNull(sectionDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
section.chapterId = chapter.id
|
||||
section.position = sectionIndex + 1
|
||||
sections.save(section)
|
||||
|
||||
sectionDto.articles.forEachIndexed { articleIndex, articleDto ->
|
||||
val article = articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
article.sectionId = section.id
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uuidOrNull(value: String): UUID? =
|
||||
runCatching { UUID.fromString(value) }.getOrNull()
|
||||
|
||||
@Transactional
|
||||
fun setChapterPublished(chapterId: UUID, published: Boolean) {
|
||||
val ch = chapters.findById(chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import ru.ysu.lms.academic.content.ContentImportResult
|
||||
import ru.ysu.lms.academic.content.ContentService
|
||||
import ru.ysu.lms.academic.content.ReorderTree
|
||||
import ru.ysu.lms.common.BadRequestException
|
||||
import java.util.UUID
|
||||
|
||||
@@ -71,6 +72,12 @@ class ContentAdminController(private val contentService: ContentService) {
|
||||
contentService.setArticlePublished(id, body.published)
|
||||
}
|
||||
|
||||
/** Новое расположение материалов (drag & drop глав, разделов и статей). */
|
||||
@PostMapping("/reorder")
|
||||
fun reorder(@RequestBody body: ReorderTree) {
|
||||
contentService.reorder(body)
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
fun import(@RequestParam("file") file: MultipartFile): ContentImportResult {
|
||||
if (file.isEmpty) throw BadRequestException("Выберите файл")
|
||||
|
||||
Reference in New Issue
Block a user