From 9681770270bb5ef57d181a819067250d6815f23c Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 16 Jul 2026 09:56:35 +0100 Subject: [PATCH] fix(dashboard): show locked notice instead of teacher empty state on lessons (#760) * fix(dashboard): show locked notice instead of teacher empty state on lessons Students viewing a locked or progression-blocked lesson were falling through to the teacher empty state with Get Started. Align lesson access checks with the shared getStudentContentLockReason helper used by exercises and the course layout, treat missing course content as inaccessible for students, and use org student role for sidebar lock blocking on the app host. Co-authored-by: Rotimi Best * fix(dashboard): only show lesson lock notice when lock reason is known Address Greptile review: avoid defaulting to teacher_locked when the lesson is missing from the content tree, and wait for course/lesson state before dismissing the student loading spinner. Co-authored-by: Rotimi Best --------- ]Co-authored-by: Rotimi Best --- .../lesson/content-navigation-actions.svelte | 8 ++- .../sidebar/course-content-tree.svelte | 7 ++- .../lib/features/course/pages/lesson.svelte | 60 +++++++++++++------ .../[id]/lessons/[lessonId]/+page.svelte | 11 +++- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte b/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte index 105cec4b..e4368b49 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte @@ -8,6 +8,7 @@ import * as Tooltip from '@cio/ui/base/tooltip'; import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress'; import { isOrgStudent } from '$lib/utils/store/app'; + import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar'; import { t } from '$lib/utils/functions/translations'; import { courseApi, lessonApi } from '$features/course/api'; import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content'; @@ -69,8 +70,10 @@ const showMarkComplete = $derived(!!lessonId && !exerciseId); const currentLessonItem = $derived(lessonId ? lessonItems.find((l) => l.id === lessonId) : null); - const isTeacherLocked = $derived($isOrgStudent && currentLessonItem && !(currentLessonItem.isUnlocked ?? false)); - const isProgressionLocked = $derived($isOrgStudent && currentLessonItem?.accessible === false && !isTeacherLocked); + const contentLockReason = $derived( + lessonId ? getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson) : null + ); + const isLessonLocked = $derived($isOrgStudent === true && contentLockReason !== null); const isVideoWatchLesson = $derived.by(() => { if (!lessonId) return false; @@ -98,7 +101,6 @@ const watchVideosRequired = $derived(lessonApi.lesson?.watchProgress?.videosRequired ?? 0); const watchVideosComplete = $derived(lessonApi.lesson?.watchProgress?.videosComplete ?? 0); - const isLessonLocked = $derived(isTeacherLocked || isProgressionLocked); const isVideoWatchComplete = $derived(isVideoWatchLesson && (lessonApi.lesson?.watchProgress?.isComplete ?? false)); const showVideoWatchCompleteState = $derived(isVideoWatchComplete || (isVideoWatchLesson && isLessonComplete)); const isMarkCompleteDisabled = $derived( diff --git a/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte b/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte index 27ff5dfd..8cb598b3 100644 --- a/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte +++ b/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte @@ -14,6 +14,7 @@ import { IconButton } from '@cio/ui/custom/icon-button'; import { RadioIcon } from '@cio/ui/custom/moving-icons'; import CourseContentIcon from '$features/course/components/course-content-icon.svelte'; + import { isOrgStudent } from '$lib/utils/store/app'; interface Props { path: string; @@ -85,7 +86,8 @@ {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} - {@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)} + {@const isLockedForStudent = + $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} - {@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)} + {@const isLockedForStudent = + $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} { - if ($isOrgStudent && currentLessonContentItem) { - return (currentLessonContentItem.isUnlocked ?? false) === false; + const contentLockReason = $derived(getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson)); + const isLessonLockedForStudent = $derived($isOrgStudent === true && contentLockReason !== null); + const isStudentLessonStateReady = $derived.by(() => { + if ($isOrgStudent !== true) { + return true; } - return (lessonApi.lesson?.isUnlocked ?? false) === false; + if (courseApi.course?.id !== courseId) { + return false; + } + + if (contentLockReason !== null) { + return true; + } + + if (currentLessonContentItem) { + return true; + } + + if (lessonApi.lesson?.id === lessonId) { + return true; + } + + return courseApi.course?.content != null; }); - const isProgressionLocked = $derived( - $isOrgStudent && currentLessonContentItem?.accessible === false && !isTeacherLocked - ); - const isLessonUnlocked = $derived(!isTeacherLocked && !isProgressionLocked); const lessonSlug = $derived(lessonApi.lesson?.slug ?? ''); const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC'); const isLiveSessionLesson = $derived(Boolean(lessonApi.lesson?.callUrl && lessonApi.lesson?.lessonAt)); @@ -450,7 +466,7 @@ {#snippet child()}
- {#if isLiveSessionLesson && mode === MODES.view && !($isOrgStudent && !isLessonUnlocked)} + {#if isLiveSessionLesson && mode === MODES.view && !isLessonLockedForStudent}
{/if} - {#if $isOrgStudent && !isLessonUnlocked} - + {#if $isOrgStudent === true && !isStudentLessonStateReady} +
+ +
+ {:else if isLessonLockedForStudent && contentLockReason} + {:else if mode === MODES.edit} {/key} + {:else if $isOrgStudent === true} + {:else} - {#if !$isOrgStudent} - - {/if} + {/if}
diff --git a/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte b/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte index 3aa76e87..7a13b964 100644 --- a/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte @@ -16,7 +16,16 @@ let { data }: Props = $props(); $effect(() => { - if (!data.lesson) return; + if (!data.lessonId) return; + + if (!data.lesson) { + if (lessonApi.lesson?.id === data.lessonId) { + lessonApi.lesson = null; + } + + return; + } + const lesson = data.lesson; if (lessonApi.lesson?.id === lesson.id) return;