feat: course section reordering & migration to course v2 (#437)

* feat: reorder sections and lessons gracefully

* convert course into version 2

* feat: get next and prev working
This commit is contained in:
Rotimi Best
2024-08-17 17:10:15 +01:00
committed by GitHub
parent e18b4ca1b1
commit 1c9dc004a0
17 changed files with 425 additions and 143 deletions
@@ -0,0 +1,63 @@
<script lang="ts">
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { supabase } from '$lib/utils/functions/supabase';
import { course } from '../../store';
import { snackbar } from '$lib/components/Snackbar/store';
export let open = false;
let isActivating = false;
const activate = async () => {
isActivating = true;
const { error } = await supabase.rpc('convert_course_to_v2', {
course_id_arg: $course.id
});
if (error) {
snackbar.error('snackbar.something');
return;
}
window.location.reload();
};
function handleClose() {
open = false;
}
</script>
<Modal
onClose={handleClose}
bind:open
width="w-[80%] md:w-[65%]"
maxWidth="max-w-xl"
modalHeading={$t(`course.navItem.lessons.section_prompt.header`)}
>
<div class="flex flex-col w-full items-center">
<div class="mb-8">
<h3 class="text-center text-2xl">
{$t('course.navItem.lessons.section_prompt.title')}
</h3>
<p class="text-center max-w-md">
{$t('course.navItem.lessons.section_prompt.description')}
</p>
</div>
<div class="flex gap-2">
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('course.navItem.lessons.section_prompt.cancel')}
onClick={handleClose}
/>
<PrimaryButton
label={$t('course.navItem.lessons.section_prompt.activate')}
onClick={activate}
isLoading={isActivating}
/>
</div>
</div>
</Modal>
@@ -75,7 +75,7 @@
use:dndzone={{
items: $lessons,
flipDurationMs,
dragDisabled: $globalStore.isStudent || !reorder,
dragDisabled: !reorder,
dropTargetStyle: {
border: '2px #1d4ed8 solid',
'border-style': 'dashed'
@@ -115,7 +115,7 @@
<a
href={$globalStore.isStudent && !lesson.is_unlocked
? $page.url.pathname
: '/courses/' + $course.id + '/lessons/' + lesson.id}
: `/courses/${$course.id}/lessons/${lesson.id}`}
class="font-medium text-black no-underline hover:underline dark:text-white {$globalStore.isStudent &&
!lesson.is_unlocked
? 'cursor-not-allowed'
@@ -145,7 +145,6 @@
<RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu size="xl">
<OverflowMenuItem
disabled={$globalStore.isStudent}
text={lesson.is_unlocked
? $t('course.navItem.lessons.add_lesson.lock')
: $t('course.navItem.lessons.add_lesson.unlock')}
@@ -1,10 +1,17 @@
<script lang="ts">
import { Edit, TrashCan, Add } from 'carbon-icons-svelte';
import { page } from '$app/stores';
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { Add, ScreenMap } from 'carbon-icons-svelte';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextChip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { t } from '$lib/utils/functions/translations';
import { globalStore } from '$lib/utils/store/app';
import { course } from '$lib/components/Course/store';
import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
import { snackbar } from '$lib/components/Snackbar/store';
import {
@@ -14,6 +21,7 @@
handleSaveLessonSection,
handleDeleteSection
} from '$lib/components/Course/components/Lesson/store/lessons';
import { updateLesson, updateLessonSection } from '$lib/utils/services/courses';
import type { Lesson } from '$lib/utils/types';
import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
@@ -25,7 +33,7 @@
lessonTitle?: string;
};
// export let reorder = false;
export let reorder = false;
export let lessonEditing: string | undefined;
let prevTitle: string | undefined;
@@ -67,7 +75,7 @@
if (!errors?.title) {
resetEdit();
} else {
snackbar.success('snackbar.success_update');
}
}
@@ -99,111 +107,247 @@
id
}));
}
/**
* Drag functionality
*/
const flipDurationMs = 200;
function handleDndConsiderColumns(e) {
$lessonSections = e.detail.items;
}
function handleDndFinalizeColumns(e) {
const prevSectionsOrder = $lessonSections.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {});
$lessonSections = e.detail.items;
e.detail.items.forEach((section, index) => {
const order = index + 1;
if (order !== prevSectionsOrder[section.id]) {
$lessonSections[index].order = order;
updateLessonSection({ order }, section.id).then((update) =>
console.log(`updated section order`, update)
);
}
});
}
function handleDndConsiderCards(cid, e) {
const colIdx = $lessonSections.findIndex((c) => c.id === cid);
$lessonSections[colIdx].lessons = e.detail.items;
$lessonSections = [...$lessonSections];
}
function handleDndFinalizeCards(cid, e) {
const colIdx = $lessonSections.findIndex((c) => c.id === cid);
const section = $lessonSections[colIdx];
const prevLessonOrder = section.lessons.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {});
$lessonSections[colIdx].lessons = e.detail.items;
$lessonSections = [...$lessonSections];
e.detail.items.forEach((lesson: Lesson, index: number) => {
const order = index + 1;
if (order !== prevLessonOrder[lesson.id] || lesson.section_id !== section.id) {
section.lessons[index].order = order;
const lessonUpdate = {
order,
section_id: $lessonSections[colIdx].id
};
updateLesson(lessonUpdate, lesson.id).then((update) =>
console.log(`updated lesson order`, update)
);
}
});
}
function getLessonOrder(lessons, id) {
const index = lessons.findIndex((lesson) => lesson.id === id);
if (index < 9) {
return '0' + (index + 1);
} else {
return index + 1;
}
}
</script>
<DeleteLessonConfirmation bind:openDeleteModal {deleteLesson} />
<section class="w-full p-3 lg:w-11/12 lg:px-4">
<div class="max-w-xl mx-auto">
{#each $lessonSections as section (section.id)}
<div class="mb-3 border border-gray-200 rounded-md">
<div
class="mb-2 px-3 py-1 min-h-[50px] border-b bg-gray-50 rounded-tl-md rounded-tr-md flex justify-between items-center"
>
{#if lessonEditing === section.id}
<TextField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
{:else}
<p class="w-4/6 font-semibold">{section.title}</p>
{/if}
<section
class="m-auto w-full p-3 lg:w-11/12 lg:px-4"
use:dndzone={{
items: $lessonSections,
flipDurationMs,
type: 'rows',
dropTargetStyle: {
border: '2px #1d4ed8 solid',
'border-style': 'dashed'
},
dragDisabled: !reorder
}}
on:consider={handleDndConsiderColumns}
on:finalize={handleDndFinalizeColumns}
>
{#each $lessonSections as section (section.id)}
<div
class="mb-3 m-auto max-w-xl border-2 {reorder
? 'border-primary-400'
: 'border-gray-200'} dark:border-neutral-600 dark:bg-neutral-800 rounded-md"
>
<div
class="mb-2 px-3 py-1 min-h-[50px] border-b bg-gray-50 dark:bg-neutral-700 rounded-tl-md rounded-tr-md flex justify-between items-center"
>
{#if lessonEditing === section.id}
<TextField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
{:else}
<p class="w-4/6 font-semibold">{section.title}</p>
{/if}
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center">
{#if lessonEditing === section.id}
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.cancel')}
variant={VARIANTS.OUTLINED}
onClick={() => {
section.title = prevTitle ?? section.title;
resetEdit();
}}
/>
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.save')}
onClick={() => onSave({ sectionId: section.id })}
/>
{:else}
<IconButton onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
<Add size={20} />
</IconButton>
<IconButton
onClick={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center">
{#if lessonEditing === section.id}
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.cancel')}
variant={VARIANTS.OUTLINED}
onClick={() => {
section.title = prevTitle ?? section.title;
resetEdit();
}}
/>
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.save')}
onClick={() => onSave({ sectionId: section.id })}
/>
{:else}
<IconButton
contained
size="small"
onClick={() => handleAddLesson(section.id)}
disabled={!!lessonEditing}
>
<Add size={20} />
</IconButton>
<OverflowMenu size="xl" flipped>
<OverflowMenuItem
text={$t('course.navItem.lessons.add_lesson.edit')}
disabled={!!lessonEditing}
>
<Edit size={20} />
</IconButton>
<IconButton
onClick={() => onDelete({ sectionId: section.id })}
disabled={!!lessonEditing}
>
<TrashCan size={20} />
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
</div>
<div class="mx-3 py-1">
{#each section.lessons as lesson (lesson.id)}
<div
class="border max-w-xl border-gray-200 px-3 py-1 min-h-[50px] rounded-md mb-2 flex items-center justify-between"
>
{#if lessonEditing === lesson.id}
<TextField
className="w-4/6"
bind:value={lesson.title}
errorMessage={errors?.title}
on:click={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
/>
{:else}
<p class="w-4/6 text-md">{lesson.title}</p>
{/if}
<OverflowMenuItem
danger
text={$t('course.navItem.lessons.add_lesson.delete')}
on:click={() => onDelete({ sectionId: section.id })}
/>
</OverflowMenu>
{/if}
</div>
</RoleBasedSecurity>
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center gap-1">
{#if lessonEditing === lesson.id}
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.cancel')}
variant={VARIANTS.OUTLINED}
onClick={() => {
lesson.title = prevTitle ?? lesson.title;
resetEdit();
<div
class="mx-3 py-1"
use:dndzone={{
items: section.lessons,
flipDurationMs,
dropTargetStyle: {
border: '2px #1d4ed8 solid',
'border-style': 'dashed'
},
dragDisabled: !reorder
}}
on:consider={(e) => handleDndConsiderCards(section.id, e)}
on:finalize={(e) => handleDndFinalizeCards(section.id, e)}
>
{#each section.lessons as lesson (lesson.id)}
<div
class="border max-w-xl {reorder
? 'border-primary-400'
: 'border-gray-200'} px-3 py-1 min-h-[50px] rounded-md mb-2 flex items-center justify-between"
animate:flip={{ duration: flipDurationMs }}
>
{#if lessonEditing === lesson.id}
<TextField className="w-4/6" bind:value={lesson.title} errorMessage={errors?.title} />
{:else}
<div class="w-4/5 flex items-center gap-2">
<TextChip
value={getLessonOrder(section.lessons, lesson.id)}
size="sm"
shape="rounded-full"
className="bg-primary-200 text-primary-600 text-xs"
/>
<div>
<a
href={$globalStore.isStudent && !lesson.is_unlocked
? $page.url.pathname
: `/courses/${$course.id}/lessons/${lesson.id}`}
class=" text-black dark:text-white {$globalStore.isStudent &&
!lesson.is_unlocked
? 'cursor-not-allowed'
: ''}"
data-sveltekit-preload-data="off"
>
{lesson.title}
</a>
<div class="mt-1 mb-3 flex items-center lg:mb-0">
<ScreenMap size={16} class="carbon-icon dark:text-white" />
<p class="ml-2 text-xs text-gray-500 dark:text-white">
{lesson?.totalExercises ? lesson?.totalExercises?.map((c) => c.count) : 0}
{$t('exercises.heading')}
</p>
</div>
</div>
</div>
{/if}
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center gap-1">
<!-- IS EDITING -->
{#if lessonEditing === lesson.id}
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.cancel')}
variant={VARIANTS.OUTLINED}
onClick={() => {
lesson.title = prevTitle ?? lesson.title;
resetEdit();
}}
/>
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.save')}
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
/>
{:else}
<OverflowMenu size="sm" flipped>
<OverflowMenuItem
text={lesson.is_unlocked
? $t('course.navItem.lessons.add_lesson.lock')
: $t('course.navItem.lessons.add_lesson.unlock')}
on:click={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
/>
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.save')}
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
<OverflowMenuItem
text={$t('course.navItem.lessons.add_lesson.edit')}
on:click={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}
/>
{:else}
<IconButton
onClick={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}
disabled={!!lessonEditing}
>
<Edit size={20} />
</IconButton>
<IconButton
onClick={() => onDelete({ lessonId: lesson.id })}
disabled={!!lessonEditing}
>
<TrashCan size={20} />
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
</div>
{:else}
{$t('course.navItem.lessons.no_lesson')}
{/each}
</div>
<OverflowMenuItem
danger
text={$t('course.navItem.lessons.add_lesson.delete')}
on:click={() => onDelete({ lessonId: lesson.id })}
/>
</OverflowMenu>
{/if}
</div>
</RoleBasedSecurity>
</div>
{:else}
{$t('course.navItem.lessons.no_lesson')}
{/each}
</div>
{/each}
</div>
</div>
{/each}
</section>
@@ -160,7 +160,7 @@
},
{
id: NAV_IDS.LESSONS,
label: $t('course.navItems.nav_lessons'),
label: $t('course.navItems.nav_content'),
to: getLessonsRoute($course.id),
hideSortIcon: false,
isPaidFeature: false,
@@ -3,7 +3,7 @@ import type { Writable } from 'svelte/store';
import { lessons, lessonSections } from './components/Lesson/store/lessons';
import { ROLE } from '$lib/utils/constants/roles';
import type { Course, GroupPerson, Lesson, LessonSection } from '$lib/utils/types';
import { COURSE_TYPE } from '$lib/utils/types';
import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
export const defaultCourse: Course = {
id: '',
@@ -20,6 +20,7 @@ export const defaultCourse: Course = {
updated_at: new Date().toDateString(),
attendance: [],
polls: [],
version: COURSE_VERSION.V2,
metadata: {
requirements: '',
description: '',
@@ -120,7 +121,6 @@ export async function setCourse(data: Course, setLesson = true) {
lessons.set(orderedLessons);
if (data.lesson_section) {
console.log('Sections', data.lesson_section);
const sections = data.lesson_section?.map((section) => {
const lessons = (data.lessons || []).filter((lesson) => lesson.section_id === section.id);
section.lessons = sortLesson(lessons);
@@ -287,7 +287,7 @@ export function updateLessonSection(section: any, sectionId: LessonSection['id']
.match({ id: sectionId });
}
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
export async function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase
.from('lesson')
.update({ ...lesson, id: undefined })
@@ -307,6 +307,14 @@ export function updateLessonCompletion(completion: LessonCompletion, shouldUpdat
}
}
export const upsertLessons = (data: { id: string; order: number }[]) => {
return supabase.from('lesson').upsert(data);
};
export const upsertLessonSections = (data: { id: string; order: number }[]) => {
return supabase.from('lesson_section').upsert(data);
};
export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Neuigkeiten",
"nav_lessons": "Unterricht",
"nav_attendance": "Teilnahme",
"nav_submissions": "Einsendungen",
"nav_marks": "Markierungen",
"nav_people": "Menschen",
"nav_certificates": "Zertifikate",
"nav_landing_page": "Landingpage",
"nav_settings": "Einstellungen"
"nav_settings": "Einstellungen",
"nav_content": "Inhalt"
},
"not_permitted": {
"header": "Keine Erlaubnis",
@@ -464,7 +464,7 @@
"course": {
"navItems": {
"nav_news_feed": "News Feed",
"nav_lessons": "Lessons",
"nav_content": "Content",
"nav_attendance": "Attendance",
"nav_submissions": "Submissions",
"nav_marks": "Marks",
@@ -504,9 +504,18 @@
"lessons": {
"heading": "Lessons",
"heading_v2": "Content",
"drag": "drag a card to reorder",
"no_lesson": "No lessons yet",
"share_your_knowledge": "Share your knowledge with the world by creating engaging lessons. Start by clicking on the Add button.",
"section_prompt": {
"cta": "Enable Sections",
"header": "Enable Sections",
"title": "We Launched Course Sections",
"description": "You can now group lessons into sections to organize your content and make learning smoother for your students.",
"activate": "Activate",
"cancel": "Cancel"
},
"add_lesson": {
"button_title": "Add",
"start_reorder": "Reorder",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Noticias",
"nav_lessons": "Lecciones",
"nav_attendance": "Asistencia",
"nav_submissions": "Envíos",
"nav_marks": "Marcas",
"nav_people": "Gente",
"nav_certificates": "Certificados",
"nav_landing_page": "Página de destino",
"nav_settings": "Ajustes"
"nav_settings": "Ajustes",
"nav_content": "Contenido"
},
"not_permitted": {
"header": "Sin autorización",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Fil d'actualité",
"nav_lessons": "Cours",
"nav_attendance": "Présence",
"nav_submissions": "Soumissions",
"nav_marks": "Des marques",
"nav_people": "Personnes",
"nav_certificates": "Certificats",
"nav_landing_page": "Page de destination",
"nav_settings": "Paramètres"
"nav_settings": "Paramètres",
"nav_content": "Contenu"
},
"not_permitted": {
"header": "Aucune autorisation",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "समाचार फ़ीड",
"nav_lessons": "पाठ",
"nav_attendance": "उपस्थिति",
"nav_submissions": "प्रविष्टियों",
"nav_marks": "निशान",
"nav_people": "लोग",
"nav_certificates": "प्रमाण पत्र",
"nav_landing_page": "लैंडिंग पृष्ठ",
"nav_settings": "समायोजन"
"nav_settings": "समायोजन",
"nav_content": "सामग्री"
},
"not_permitted": {
"header": "अनुमति नहीं",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Notícias",
"nav_lessons": "Lições",
"nav_attendance": "Comparecimento",
"nav_submissions": "Envios",
"nav_marks": "Marcas",
"nav_people": "Pessoas",
"nav_certificates": "Certificados",
"nav_landing_page": "Página inicial",
"nav_settings": "Configurações"
"nav_settings": "Configurações",
"nav_content": "Contente"
},
"not_permitted": {
"header": "Sem permissão",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Новостная лента",
"nav_lessons": "Уроки",
"nav_attendance": "посещаемость",
"nav_submissions": "Материалы",
"nav_marks": "Метки",
"nav_people": "Люди",
"nav_certificates": "Сертификаты",
"nav_landing_page": "Целевая страница",
"nav_settings": "Настройки"
"nav_settings": "Настройки",
"nav_content": "Содержание"
},
"not_permitted": {
"header": "Нет разрешения",
@@ -801,14 +801,14 @@
},
"navItems": {
"nav_news_feed": "Nguồn cấp tin tức",
"nav_lessons": "Những bài học",
"nav_attendance": "Sự tham dự",
"nav_submissions": "bài nộp",
"nav_marks": "Điểm",
"nav_people": "Mọi người",
"nav_certificates": "Chứng chỉ",
"nav_landing_page": "Trang đích",
"nav_settings": "Cài đặt"
"nav_settings": "Cài đặt",
"nav_content": "Nội dung"
},
"not_permitted": {
"header": "Không cho phép",
@@ -4,6 +4,7 @@
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import {
lessons,
@@ -18,11 +19,11 @@
import { t } from '$lib/utils/functions/translations';
import { goto } from '$app/navigation';
import { COURSE_VERSION } from '$lib/utils/types';
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
import { profile } from '$lib/utils/store/user.js';
import { profile } from '$lib/utils/store/user';
import LessonList from '$lib/components/Course/components/Lesson/LessonList.svelte';
import LessonSectionList from '$lib/components/Course/components/Lesson/LessonSectionList.svelte';
import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte';
import ActivateSectionsModal from '$lib/components/Course/components/Lesson/ActivateSectionsModal.svelte';
export let data;
@@ -33,6 +34,7 @@
let openDeleteModal: boolean = false;
let isFetching: boolean = false;
let reorder = false;
let activateSections = false;
function addLesson() {
$handleAddLessonWidget.open = true;
@@ -44,8 +46,22 @@
return completion?.find((c) => c.profile_id === $profile.id);
}
const getLessons = () => {
if ($course.version === COURSE_VERSION.V1) {
return $lessons;
} else {
const _lessons: Lesson[] = [];
$lessonSections.forEach((section) => {
_lessons.push(...section.lessons);
});
return _lessons;
}
};
function findFirstIncompleteLesson() {
return $lessons.find(
return getLessons().find(
(lesson) => !hasUserCompletedLesson(lesson.lesson_completion) && lesson.is_unlocked === true
);
}
@@ -70,30 +86,38 @@
<NewLessonModal />
<ActivateSectionsModal bind:open={activateSections} />
<DeleteLessonConfirmation
bind:openDeleteModal
deleteLesson={() => handleDelete(lessonToDelete?.id)}
/>
<CourseContainer bind:isFetching bind:courseId={data.courseId}>
<PageNav title={$t('course.navItem.lessons.heading')}>
<PageNav title={$t('course.navItem.lessons.heading_v2')}>
<div slot="widget" class="flex w-full justify-end gap-2">
<RoleBasedSecurity allowedRoles={[1, 2]}>
{#if $course.version === COURSE_VERSION.V1}
<PrimaryButton
label={$t(`course.navItem.lessons.section_prompt.cta`)}
variant={VARIANTS.OUTLINED}
onClick={() => (activateSections = !activateSections)}
isDisabled={!!lessonEditing}
/>
{/if}
<PrimaryButton
label={$t(
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
)}
variant={VARIANTS.OUTLINED}
onClick={() => (reorder = !reorder)}
isDisabled={!!lessonEditing}
/>
<PrimaryButton
label={$t('course.navItem.lessons.add_lesson.button_title')}
onClick={addLesson}
isDisabled={!!lessonEditing}
/>
{#if $course.version === COURSE_VERSION.V1}
<PrimaryButton
label={$t(
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
)}
variant={VARIANTS.OUTLINED}
onClick={() => (reorder = !reorder)}
isDisabled={!!lessonEditing}
/>
{/if}
</RoleBasedSecurity>
</div>
</PageNav>
@@ -119,7 +143,7 @@
{#if $course.version === COURSE_VERSION.V1}
<LessonList {reorder} {lessonEditing} bind:lessonToDelete bind:openDeleteModal />
{:else if $course.version === COURSE_VERSION.V2}
<LessonSectionList {lessonEditing} />
<LessonSectionList {reorder} {lessonEditing} />
{/if}
{:else}
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
@@ -31,12 +31,13 @@
import {
lesson,
lessons,
lessonByTranslation
lessonByTranslation,
lessonSections
} from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment';
import { currentOrg } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
import type { LessonCompletion } from '$lib/utils/types';
import { COURSE_VERSION, type LessonCompletion, type Lesson } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
import { t } from '$lib/utils/functions/translations';
@@ -249,20 +250,36 @@
$apps.open = true;
}
const isNextOrPrevDisabled = (lessonId: string, isPrev: boolean) => {
const index = $lessons.findIndex((lesson) => lesson.id === lessonId);
const getLessons = () => {
if ($course.version === COURSE_VERSION.V1) {
return $lessons;
} else {
const _lessons: Lesson[] = [];
return isPrev ? !$lessons[index - 1] : !$lessons[index + 1];
$lessonSections.forEach((section) => {
_lessons.push(...section.lessons);
});
return _lessons;
}
};
const isNextOrPrevDisabled = (lessonId: string, isPrev: boolean) => {
const _lessons = getLessons();
const index = _lessons.findIndex((lesson) => lesson.id === lessonId);
return isPrev ? !_lessons[index - 1] : !_lessons[index + 1];
};
const goToNextOrPrevLesson = (lessonId: string, isPrev: boolean) => {
const _lessons = getLessons();
const isDisabled = isNextOrPrevDisabled(lessonId, isPrev);
// Always use early return
if (isDisabled) return;
const index = $lessons.findIndex((lesson) => lesson.id === lessonId);
const nextOrPrevLesson = isPrev ? $lessons[index - 1] : $lessons[index + 1];
const index = _lessons.findIndex((lesson) => lesson.id === lessonId);
const nextOrPrevLesson = isPrev ? _lessons[index - 1] : _lessons[index + 1];
const isLocked = $globalStore.isStudent && !nextOrPrevLesson.is_unlocked;
@@ -124,4 +124,22 @@ using (is_user_in_group_with_role(( SELECT course.group_id
with check (is_user_in_group_with_role(( SELECT course.group_id
FROM course
WHERE (course.id = lesson_section.course_id)
LIMIT 1)));
LIMIT 1)));
CREATE
OR REPLACE FUNCTION convert_course_to_v2 (course_id uuid) RETURNS void AS $$
DECLARE
new_section_id bigint;
BEGIN
UPDATE course
SET version = 'V2'
WHERE id = course_id;
INSERT INTO lesson_section (title, course_id) VALUES ('First Section [edit me]', course_id) RETURNING id INTO new_section_id;
UPDATE lesson
SET section_id = new_section_id
WHERE course_id = course_id;
END;
$$ LANGUAGE plpgsql;