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 <[email protected]> * 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 <[email protected]> --------- ]Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
c9b7c79365
commit
9681770270
+5
-3
@@ -8,6 +8,7 @@
|
|||||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||||
import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress';
|
import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress';
|
||||||
import { isOrgStudent } from '$lib/utils/store/app';
|
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 { t } from '$lib/utils/functions/translations';
|
||||||
import { courseApi, lessonApi } from '$features/course/api';
|
import { courseApi, lessonApi } from '$features/course/api';
|
||||||
import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content';
|
import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content';
|
||||||
@@ -69,8 +70,10 @@
|
|||||||
const showMarkComplete = $derived(!!lessonId && !exerciseId);
|
const showMarkComplete = $derived(!!lessonId && !exerciseId);
|
||||||
|
|
||||||
const currentLessonItem = $derived(lessonId ? lessonItems.find((l) => l.id === lessonId) : null);
|
const currentLessonItem = $derived(lessonId ? lessonItems.find((l) => l.id === lessonId) : null);
|
||||||
const isTeacherLocked = $derived($isOrgStudent && currentLessonItem && !(currentLessonItem.isUnlocked ?? false));
|
const contentLockReason = $derived(
|
||||||
const isProgressionLocked = $derived($isOrgStudent && currentLessonItem?.accessible === false && !isTeacherLocked);
|
lessonId ? getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson) : null
|
||||||
|
);
|
||||||
|
const isLessonLocked = $derived($isOrgStudent === true && contentLockReason !== null);
|
||||||
const isVideoWatchLesson = $derived.by(() => {
|
const isVideoWatchLesson = $derived.by(() => {
|
||||||
if (!lessonId) return false;
|
if (!lessonId) return false;
|
||||||
|
|
||||||
@@ -98,7 +101,6 @@
|
|||||||
|
|
||||||
const watchVideosRequired = $derived(lessonApi.lesson?.watchProgress?.videosRequired ?? 0);
|
const watchVideosRequired = $derived(lessonApi.lesson?.watchProgress?.videosRequired ?? 0);
|
||||||
const watchVideosComplete = $derived(lessonApi.lesson?.watchProgress?.videosComplete ?? 0);
|
const watchVideosComplete = $derived(lessonApi.lesson?.watchProgress?.videosComplete ?? 0);
|
||||||
const isLessonLocked = $derived(isTeacherLocked || isProgressionLocked);
|
|
||||||
const isVideoWatchComplete = $derived(isVideoWatchLesson && (lessonApi.lesson?.watchProgress?.isComplete ?? false));
|
const isVideoWatchComplete = $derived(isVideoWatchLesson && (lessonApi.lesson?.watchProgress?.isComplete ?? false));
|
||||||
const showVideoWatchCompleteState = $derived(isVideoWatchComplete || (isVideoWatchLesson && isLessonComplete));
|
const showVideoWatchCompleteState = $derived(isVideoWatchComplete || (isVideoWatchLesson && isLessonComplete));
|
||||||
const isMarkCompleteDisabled = $derived(
|
const isMarkCompleteDisabled = $derived(
|
||||||
|
|||||||
+5
-2
@@ -14,6 +14,7 @@
|
|||||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||||
import { RadioIcon } from '@cio/ui/custom/moving-icons';
|
import { RadioIcon } from '@cio/ui/custom/moving-icons';
|
||||||
import CourseContentIcon from '$features/course/components/course-content-icon.svelte';
|
import CourseContentIcon from '$features/course/components/course-content-icon.svelte';
|
||||||
|
import { isOrgStudent } from '$lib/utils/store/app';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -85,7 +86,8 @@
|
|||||||
<Sidebar.MenuSubButton isActive={(path || page.url.pathname).includes(contentItem.id)}>
|
<Sidebar.MenuSubButton isActive={(path || page.url.pathname).includes(contentItem.id)}>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
||||||
{@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)}
|
{@const isLockedForStudent =
|
||||||
|
$isOrgStudent === true && (isContentLocked || contentItem.accessible === false)}
|
||||||
<a
|
<a
|
||||||
href={resolve(getContentRoute(id, contentItem), {})}
|
href={resolve(getContentRoute(id, contentItem), {})}
|
||||||
aria-disabled={isLockedForStudent}
|
aria-disabled={isLockedForStudent}
|
||||||
@@ -137,7 +139,8 @@
|
|||||||
<Sidebar.MenuSubButton isActive={(path || page.url.pathname).includes(contentItem.id)}>
|
<Sidebar.MenuSubButton isActive={(path || page.url.pathname).includes(contentItem.id)}>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
||||||
{@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)}
|
{@const isLockedForStudent =
|
||||||
|
$isOrgStudent === true && (isContentLocked || contentItem.accessible === false)}
|
||||||
<a
|
<a
|
||||||
href={resolve(getContentRoute(id, contentItem), {})}
|
href={resolve(getContentRoute(id, contentItem), {})}
|
||||||
aria-disabled={isLockedForStudent}
|
aria-disabled={isLockedForStudent}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import MODES from '$lib/utils/constants/mode';
|
import MODES from '$lib/utils/constants/mode';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { isOrgStudent, isStudentExperience } from '$lib/utils/store/app';
|
import { isOrgStudent, isStudentExperience } from '$lib/utils/store/app';
|
||||||
|
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { snackbar } from '$features/ui/snackbar/store';
|
import { snackbar } from '$features/ui/snackbar/store';
|
||||||
import { RefreshPageData, UnsavedChanges } from '$features/ui';
|
import { RefreshPageData, UnsavedChanges } from '$features/ui';
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
import * as Page from '@cio/ui/base/page';
|
import * as Page from '@cio/ui/base/page';
|
||||||
import * as UnderlineTabs from '@cio/ui/custom/underline-tabs';
|
import * as UnderlineTabs from '@cio/ui/custom/underline-tabs';
|
||||||
import { Empty } from '@cio/ui/custom/empty';
|
import { Empty } from '@cio/ui/custom/empty';
|
||||||
|
import { Spinner } from '@cio/ui/base/spinner';
|
||||||
import { RoleBasedSecurity } from '$features/ui';
|
import { RoleBasedSecurity } from '$features/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -83,17 +85,31 @@
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
const lessonTitle = $derived(currentLessonContentItem?.title || lessonApi.lesson?.title || 'Lesson');
|
const lessonTitle = $derived(currentLessonContentItem?.title || lessonApi.lesson?.title || 'Lesson');
|
||||||
const isTeacherLocked = $derived.by(() => {
|
const contentLockReason = $derived(getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson));
|
||||||
if ($isOrgStudent && currentLessonContentItem) {
|
const isLessonLockedForStudent = $derived($isOrgStudent === true && contentLockReason !== null);
|
||||||
return (currentLessonContentItem.isUnlocked ?? false) === false;
|
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 lessonSlug = $derived(lessonApi.lesson?.slug ?? '');
|
||||||
const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC');
|
const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC');
|
||||||
const isLiveSessionLesson = $derived(Boolean(lessonApi.lesson?.callUrl && lessonApi.lesson?.lessonAt));
|
const isLiveSessionLesson = $derived(Boolean(lessonApi.lesson?.callUrl && lessonApi.lesson?.lessonAt));
|
||||||
@@ -450,7 +466,7 @@
|
|||||||
<Page.Body>
|
<Page.Body>
|
||||||
{#snippet child()}
|
{#snippet child()}
|
||||||
<div class={`overflow-x-hidden pb-6 ${mode === MODES.edit ? 'lg:w-full xl:w-11/12' : 'mx-auto w-full max-w-3xl'}`}>
|
<div class={`overflow-x-hidden pb-6 ${mode === MODES.edit ? 'lg:w-full xl:w-11/12' : 'mx-auto w-full max-w-3xl'}`}>
|
||||||
{#if isLiveSessionLesson && mode === MODES.view && !($isOrgStudent && !isLessonUnlocked)}
|
{#if isLiveSessionLesson && mode === MODES.view && !isLessonLockedForStudent}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<LiveSessionCard
|
<LiveSessionCard
|
||||||
title={lessonTitle}
|
title={lessonTitle}
|
||||||
@@ -461,11 +477,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $isOrgStudent && !isLessonUnlocked}
|
{#if $isOrgStudent === true && !isStudentLessonStateReady}
|
||||||
<StudentContentLockedNotice
|
<div class="flex justify-center py-16">
|
||||||
reason={isProgressionLocked ? 'progression_locked' : 'teacher_locked'}
|
<Spinner />
|
||||||
contentType={ContentType.Lesson}
|
</div>
|
||||||
/>
|
{:else if isLessonLockedForStudent && contentLockReason}
|
||||||
|
<StudentContentLockedNotice reason={contentLockReason} contentType={ContentType.Lesson} />
|
||||||
{:else if mode === MODES.edit}
|
{:else if mode === MODES.edit}
|
||||||
<UnderlineTabs.Root
|
<UnderlineTabs.Root
|
||||||
bind:value={currentTabValue}
|
bind:value={currentTabValue}
|
||||||
@@ -541,6 +558,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
|
{:else if $isOrgStudent === true}
|
||||||
|
<Empty
|
||||||
|
title={$t('course.navItem.lessons.materials.no_content')}
|
||||||
|
icon={VideoIcon}
|
||||||
|
variant="page"
|
||||||
|
class="text-center"
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Empty
|
<Empty
|
||||||
title={$t('course.navItem.lessons.materials.body_heading')}
|
title={$t('course.navItem.lessons.materials.body_heading')}
|
||||||
@@ -554,11 +578,9 @@
|
|||||||
variant="page"
|
variant="page"
|
||||||
class="text-center"
|
class="text-center"
|
||||||
>
|
>
|
||||||
{#if !$isOrgStudent}
|
<Button onclick={toggleMode}>
|
||||||
<Button onclick={toggleMode}>
|
{$t('course.navItem.lessons.materials.get_started')}
|
||||||
{$t('course.navItem.lessons.materials.get_started')}
|
</Button>
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</Empty>
|
</Empty>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,16 @@
|
|||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
$effect(() => {
|
$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;
|
const lesson = data.lesson;
|
||||||
if (lessonApi.lesson?.id === lesson.id) return;
|
if (lessonApi.lesson?.id === lesson.id) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user