Learn UI: prev/next navigation, sidebar outline, editing on every page; merge import; defense date
- article page: ← / → navigation across the whole material tree (including between sections), sticky sidebar outline with the current article highlighted - editing on chapter, section and article pages: rename, publish/hide, delete, create section/article, move articles while deleting a section; forms return to the page they were opened from (validated internal `back` parameter) - article page opens the edit dialog directly via ?edit=1 (used by "Править" links on section/chapter pages) - import: optional sync flag — with it stale sections/articles of archived chapters are deleted (previous behaviour), without it import only adds and updates, so material edited in the UI is not overwritten - student area: lab page and lab cards now show the defense date next to the submission deadline - fix SpEL crash on the student lab page (`and` with nullable strings)
This commit is contained in:
@@ -400,7 +400,7 @@ class ContentService(
|
||||
* картинки в assets/.
|
||||
*/
|
||||
@Transactional
|
||||
fun importZip(filename: String, bytes: ByteArray): ContentImportResult {
|
||||
fun importZip(filename: String, bytes: ByteArray, sync: Boolean = true): ContentImportResult {
|
||||
if (!filename.lowercase().endsWith(".zip")) {
|
||||
throw BadRequestException("Ожидается .zip архив")
|
||||
}
|
||||
@@ -509,8 +509,10 @@ class ContentService(
|
||||
artCount += 1
|
||||
}
|
||||
|
||||
// Синхронизация: в главах из архива удаляем разделы и статьи, которых в архиве нет.
|
||||
for (chapterId in touchedChapters) {
|
||||
// Синхронизация (если включена): в главах из архива удаляем разделы и статьи,
|
||||
// которых в архиве нет. В режиме merge (sync = false) ничего не удаляем —
|
||||
// удобно догружать отдельные статьи, не затрагивая правки, сделанные в интерфейсе.
|
||||
for (chapterId in if (sync) touchedChapters else emptySet()) {
|
||||
for (section in sections.findByChapterIdOrderByPositionAscIdAsc(chapterId)) {
|
||||
if (section.id in seenSections) {
|
||||
for (article in articles.findBySectionIdOrderByPositionAscIdAsc(section.id)) {
|
||||
|
||||
@@ -79,9 +79,12 @@ class ContentAdminController(private val contentService: ContentService) {
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
fun import(@RequestParam("file") file: MultipartFile): ContentImportResult {
|
||||
fun import(
|
||||
@RequestParam("file") file: MultipartFile,
|
||||
@RequestParam(value = "sync", defaultValue = "true") sync: Boolean,
|
||||
): ContentImportResult {
|
||||
if (file.isEmpty) throw BadRequestException("Выберите файл")
|
||||
return contentService.importZip(file.originalFilename ?: "content.zip", file.bytes)
|
||||
return contentService.importZip(file.originalFilename ?: "content.zip", file.bytes, sync)
|
||||
}
|
||||
|
||||
@GetMapping("/articles")
|
||||
|
||||
Reference in New Issue
Block a user