Merge pull request #1623 from JoeBrar/feature/reorder-chapters

feat: added chapter re-ordering functionality for courses
This commit is contained in:
Jannat Patel
2025-07-10 12:20:04 +05:30
committed by GitHub
2 changed files with 163 additions and 104 deletions

View File

@@ -23,119 +23,135 @@
'border-2 rounded-md py-2 px-2': showOutline && outline.data?.length, 'border-2 rounded-md py-2 px-2': showOutline && outline.data?.length,
}" }"
> >
<Disclosure <Draggable
v-slot="{ open }" :list="outline.data"
v-for="(chapter, index) in outline.data" :disabled="!allowEdit"
:key="chapter.name" item-key="name"
:defaultOpen="openChapterDetail(chapter.idx)" group="chapters"
@end="updateChapterOrder"
> >
<DisclosureButton ref="" class="flex items-center w-full p-2 group"> <template #item="{ element: chapter, index }">
<ChevronRight <div class="chapter-item">
:class="{ <Disclosure
'rotate-90 transform duration-200': open, v-slot="{ open }"
'duration-200': !open, :key="chapter.name"
hidden: chapter.is_scorm_package, :defaultOpen="openChapterDetail(chapter.idx)"
open: index == 1, >
}" <DisclosureButton
class="h-4 w-4 text-ink-gray-9 stroke-1" ref=""
/> class="flex items-center w-full p-2 group"
<div
class="text-base text-left text-ink-gray-9 font-medium leading-5 ml-2"
@click="redirectToChapter(chapter)"
>
{{ chapter.title }}
</div>
<div class="flex ml-auto space-x-4">
<Tooltip :text="__('Edit Chapter')" placement="bottom">
<FilePenLine
v-if="allowEdit"
@click.prevent="openChapterModal(chapter)"
class="h-4 w-4 text-ink-gray-9 invisible group-hover:visible"
/>
</Tooltip>
<Tooltip :text="__('Delete Chapter')" placement="bottom">
<Trash2
v-if="allowEdit"
@click.prevent="trashChapter(chapter.name)"
class="h-4 w-4 text-ink-red-3 invisible group-hover:visible"
/>
</Tooltip>
</div>
</DisclosureButton>
<DisclosurePanel v-if="!chapter.is_scorm_package">
<Draggable
v-if="!chapter.is_scorm_package"
:list="chapter.lessons"
:disabled="!allowEdit"
item-key="name"
group="items"
@end="updateOutline"
:data-chapter="chapter.name"
>
<template #item="{ element: lesson }">
<div
class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
:class="
isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
"
> >
<router-link <ChevronRight
:to="{ :class="{
name: allowEdit ? 'LessonForm' : 'Lesson', 'rotate-90 transform duration-200': open,
params: { 'duration-200': !open,
courseName: courseName, hidden: chapter.is_scorm_package,
chapterNumber: lesson.number.split('.')[0], open: index == 1,
lessonNumber: lesson.number.split('.')[1],
},
}" }"
class="h-4 w-4 text-ink-gray-9 stroke-1"
/>
<div
class="text-base text-left text-ink-gray-9 font-medium leading-5 ml-2"
@click="redirectToChapter(chapter)"
> >
<div class="flex items-center text-sm leading-5 group"> {{ chapter.title }}
<MonitorPlay </div>
v-if="lesson.icon === 'icon-youtube'" <div class="flex ml-auto space-x-4">
class="h-4 w-4 stroke-1 mr-2" <Tooltip :text="__('Edit Chapter')" placement="bottom">
<FilePenLine
v-if="allowEdit"
@click.prevent="openChapterModal(chapter)"
class="h-4 w-4 text-ink-gray-9 invisible group-hover:visible"
/> />
<HelpCircle </Tooltip>
v-else-if="lesson.icon === 'icon-quiz'" <Tooltip :text="__('Delete Chapter')" placement="bottom">
class="h-4 w-4 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
/>
{{ lesson.title }}
<Trash2 <Trash2
v-if="allowEdit" v-if="allowEdit"
@click.prevent="trashLesson(lesson.name, chapter.name)" @click.prevent="trashChapter(chapter.name)"
class="h-4 w-4 text-ink-red-3 ml-auto invisible group-hover:visible" class="h-4 w-4 text-ink-red-3 invisible group-hover:visible"
/> />
<Check </Tooltip>
v-if="lesson.is_complete" </div>
class="h-4 w-4 text-green-700 ml-2" </DisclosureButton>
/> <DisclosurePanel v-if="!chapter.is_scorm_package">
</div> <Draggable
</router-link> v-if="!chapter.is_scorm_package"
</div> :list="chapter.lessons"
</template> :disabled="!allowEdit"
</Draggable> item-key="name"
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8"> group="items"
<router-link @end="updateOutline"
v-if="!chapter.is_scorm_package" :data-chapter="chapter.name"
:to="{ >
name: 'LessonForm', <template #item="{ element: lesson }">
params: { <div
courseName: courseName, class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
chapterNumber: chapter.idx, :class="
lessonNumber: chapter.lessons.length + 1, isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
}, "
}" >
> <router-link
<Button> :to="{
{{ __('Add Lesson') }} name: allowEdit ? 'LessonForm' : 'Lesson',
</Button> params: {
</router-link> courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
},
}"
>
<div class="flex items-center text-sm leading-5 group">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 stroke-1 mr-2"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
/>
{{ lesson.title }}
<Trash2
v-if="allowEdit"
@click.prevent="
trashLesson(lesson.name, chapter.name)
"
class="h-4 w-4 text-ink-red-3 ml-auto invisible group-hover:visible"
/>
<Check
v-if="lesson.is_complete"
class="h-4 w-4 text-green-700 ml-2"
/>
</div>
</router-link>
</div>
</template>
</Draggable>
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<router-link
v-if="!chapter.is_scorm_package"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: chapter.idx,
lessonNumber: chapter.lessons.length + 1,
},
}"
>
<Button>
{{ __('Add Lesson') }}
</Button>
</router-link>
</div>
</DisclosurePanel>
</Disclosure>
</div> </div>
</DisclosurePanel> </template>
</Disclosure> </Draggable>
</div> </div>
</div> </div>
<ChapterModal <ChapterModal
@@ -242,6 +258,20 @@ const updateLessonIndex = createResource({
}, },
}) })
const updateChapterIndex = createResource({
url: 'lms.lms.api.update_chapter_index',
makeParams(values) {
return {
chapter: values.chapter,
course: values.course,
idx: values.idx,
}
},
onSuccess() {
toast.success(__('Chapter moved successfully'))
},
})
const trashLesson = (lessonName, chapterName) => { const trashLesson = (lessonName, chapterName) => {
$dialog({ $dialog({
title: __('Delete this lesson?'), title: __('Delete this lesson?'),
@@ -287,6 +317,14 @@ const updateOutline = (e) => {
}) })
} }
const updateChapterOrder = (e) => {
updateChapterIndex.submit({
chapter: e.item.__draggable_context.element.name,
course: props.courseName,
idx: e.newIndex,
})
}
const deleteChapter = createResource({ const deleteChapter = createResource({
url: 'lms.lms.api.delete_chapter', url: 'lms.lms.api.delete_chapter',
makeParams(values) { makeParams(values) {

View File

@@ -676,6 +676,27 @@ def update_index(lessons, chapter):
) )
@frappe.whitelist()
def update_chapter_index(chapter, course, idx):
"""Update the index of a chapter within a course"""
chapters = frappe.get_all(
"Chapter Reference",
{"parent": course},
pluck="chapter",
order_by="idx",
)
if chapter in chapters:
chapters.remove(chapter)
chapters.insert(idx, chapter)
for i, chapter_name in enumerate(chapters):
frappe.db.set_value(
"Chapter Reference", {"chapter": chapter_name, "parent": course}, "idx", i + 1
)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_categories(doctype, filters): def get_categories(doctype, filters):
categoryOptions = [] categoryOptions = []