From 3ef286ed39d58760a84f6f411e74ab575d63da2d Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 16 Jul 2026 11:29:59 +0100 Subject: [PATCH] fix(dashboard): use learner view for lesson lock empty states (#764) Lock checks gated on isOrgStudent missed org-site students where isStudentExperience is true but org role may be unset. Introduce isCourseLearnerView (student experience OR org student) and mirror the exercise page branch structure so locked lessons always show StudentContentLockedNotice instead of the teacher empty state. Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- .../lesson/content-navigation-actions.svelte | 16 ++++-- .../sidebar/course-content-tree.svelte | 7 ++- .../lib/features/course/pages/lesson.svelte | 53 +++++++++++++------ apps/dashboard/src/lib/utils/store/app.ts | 13 +++++ .../routes/(app)/courses/[id]/+layout.svelte | 4 +- 5 files changed, 67 insertions(+), 26 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 e4368b49..0b6c8300 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 @@ -7,7 +7,7 @@ import { Button } from '@cio/ui/base/button'; 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 { isCourseLearnerView } 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'; @@ -52,6 +52,16 @@ function goToContent(target: CourseContentItem | null) { if (!target) return; + + if ($isCourseLearnerView) { + const targetType = target.type === ContentType.Lesson ? ContentType.Lesson : ContentType.Exercise; + const lockReason = getStudentContentLockReason(courseApi.course, target.id, targetType); + + if (lockReason) { + return; + } + } + const courseIdResolved = courseApi.course?.id; if (!courseIdResolved) return; const path = getContentRoute(courseIdResolved, target); @@ -73,7 +83,7 @@ const contentLockReason = $derived( lessonId ? getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson) : null ); - const isLessonLocked = $derived($isOrgStudent === true && contentLockReason !== null); + const isLessonLocked = $derived($isCourseLearnerView && contentLockReason !== null); const isVideoWatchLesson = $derived.by(() => { if (!lessonId) return false; @@ -107,7 +117,7 @@ isMarkingComplete || isLessonLocked || isLessonComplete || isVideoWatchLesson ); const showWatchProgress = $derived( - !!lessonId && $isOrgStudent && isVideoWatchLesson && !showVideoWatchCompleteState && !isLessonLocked + !!lessonId && $isCourseLearnerView && isVideoWatchLesson && !showVideoWatchCompleteState && !isLessonLocked ); const watchProgressTooltip = $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 fe1f9190..e564df87 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,7 +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'; + import { isCourseLearnerView } from '$lib/utils/store/app'; interface Props { path: string; @@ -99,7 +99,7 @@ {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} {@const isLockedForStudent = - $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} + $isCourseLearnerView && (isContentLocked || contentItem.accessible === false)} {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} - {@const isLockedForStudent = - $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} + {@const isLockedForStudent = $isCourseLearnerView && (isContentLocked || contentItem.accessible === false)} { - if ($isOrgStudent !== true) { + if (!$isCourseLearnerView) { return true; } @@ -466,7 +465,7 @@ {#snippet child()}
- {#if isLiveSessionLesson && mode === MODES.view && !isLessonLockedForStudent} + {#if isLiveSessionLesson && mode === MODES.view && !($isCourseLearnerView && contentLockReason)}
{/if} - {#if $isOrgStudent === true && !isStudentLessonStateReady} -
- -
- {:else if isLessonLockedForStudent && contentLockReason} - + {#if $isCourseLearnerView} + {#if !isStudentLessonStateReady} +
+ +
+ {:else if contentLockReason} + + {:else if lessonApi.lesson && !isMaterialsEmpty} + {#key lessonId} +
+ {#if !hasLessonVideos} + + {/if} + + {#each viewModeComponents as Component, index (index)} + + {/each} + + {#if $currentOrg.customization?.apps?.comments} +
+ + + {/if} +
+ {/key} + {:else} + + {/if} {:else if mode === MODES.edit} {/key} - {:else if $isOrgStudent === true} - {:else} { + return $isStudentExperience || $isOrgStudent === true; + } +); + /** * The root path for navigation: '/lms' for students, '/org/{siteName}' for admin/teacher */ diff --git a/apps/dashboard/src/routes/(app)/courses/[id]/+layout.svelte b/apps/dashboard/src/routes/(app)/courses/[id]/+layout.svelte index bf5b4bfd..189e4487 100644 --- a/apps/dashboard/src/routes/(app)/courses/[id]/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/courses/[id]/+layout.svelte @@ -25,7 +25,7 @@ import { page } from '$app/state'; import { profile } from '$lib/utils/store/user'; import { isOrgAdmin } from '$lib/utils/store/org'; - import { isOrgStudent } from '$lib/utils/store/app'; + import { isCourseLearnerView } from '$lib/utils/store/app'; import { t } from '$lib/utils/functions/translations'; import { ContentType } from '@cio/utils/constants/content'; import type { CourseMember } from '$features/course/utils/types'; @@ -104,7 +104,7 @@ const showContentAskAiBar = $derived( isCourseReady && isLessonOrExercisePage && - !($isOrgStudent && isContentLockedForStudent) && + !($isCourseLearnerView && isContentLockedForStudent) && sidePanel.activePanelId !== AI_ASSISTANT_PANEL_ID );