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 <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Cursor Agent
Rotimi Best
parent
7a867dad45
commit
3ef286ed39
+13
-3
@@ -7,7 +7,7 @@
|
|||||||
import { Button } from '@cio/ui/base/button';
|
import { Button } from '@cio/ui/base/button';
|
||||||
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 { isCourseLearnerView } from '$lib/utils/store/app';
|
||||||
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
|
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';
|
||||||
@@ -52,6 +52,16 @@
|
|||||||
|
|
||||||
function goToContent(target: CourseContentItem | null) {
|
function goToContent(target: CourseContentItem | null) {
|
||||||
if (!target) return;
|
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;
|
const courseIdResolved = courseApi.course?.id;
|
||||||
if (!courseIdResolved) return;
|
if (!courseIdResolved) return;
|
||||||
const path = getContentRoute(courseIdResolved, target);
|
const path = getContentRoute(courseIdResolved, target);
|
||||||
@@ -73,7 +83,7 @@
|
|||||||
const contentLockReason = $derived(
|
const contentLockReason = $derived(
|
||||||
lessonId ? getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson) : null
|
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(() => {
|
const isVideoWatchLesson = $derived.by(() => {
|
||||||
if (!lessonId) return false;
|
if (!lessonId) return false;
|
||||||
|
|
||||||
@@ -107,7 +117,7 @@
|
|||||||
isMarkingComplete || isLessonLocked || isLessonComplete || isVideoWatchLesson
|
isMarkingComplete || isLessonLocked || isLessonComplete || isVideoWatchLesson
|
||||||
);
|
);
|
||||||
const showWatchProgress = $derived(
|
const showWatchProgress = $derived(
|
||||||
!!lessonId && $isOrgStudent && isVideoWatchLesson && !showVideoWatchCompleteState && !isLessonLocked
|
!!lessonId && $isCourseLearnerView && isVideoWatchLesson && !showVideoWatchCompleteState && !isLessonLocked
|
||||||
);
|
);
|
||||||
|
|
||||||
const watchProgressTooltip = $derived(
|
const watchProgressTooltip = $derived(
|
||||||
|
|||||||
+3
-4
@@ -14,7 +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';
|
import { isCourseLearnerView } from '$lib/utils/store/app';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
{@const isContentLocked = (contentItem.isUnlocked ?? true) === false}
|
||||||
{@const isLockedForStudent =
|
{@const isLockedForStudent =
|
||||||
$isOrgStudent === true && (isContentLocked || contentItem.accessible === false)}
|
$isCourseLearnerView && (isContentLocked || contentItem.accessible === false)}
|
||||||
<a
|
<a
|
||||||
href={resolve(getContentRoute(id, contentItem), {})}
|
href={resolve(getContentRoute(id, contentItem), {})}
|
||||||
aria-disabled={isLockedForStudent}
|
aria-disabled={isLockedForStudent}
|
||||||
@@ -151,8 +151,7 @@
|
|||||||
<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 =
|
{@const isLockedForStudent = $isCourseLearnerView && (isContentLocked || contentItem.accessible === false)}
|
||||||
$isOrgStudent === true && (isContentLocked || contentItem.accessible === false)}
|
|
||||||
<a
|
<a
|
||||||
href={resolve(getContentRoute(id, contentItem), {})}
|
href={resolve(getContentRoute(id, contentItem), {})}
|
||||||
aria-disabled={isLockedForStudent}
|
aria-disabled={isLockedForStudent}
|
||||||
|
|||||||
@@ -16,7 +16,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 { isCourseLearnerView, isStudentExperience } from '$lib/utils/store/app';
|
||||||
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
|
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';
|
||||||
@@ -86,9 +86,8 @@
|
|||||||
);
|
);
|
||||||
const lessonTitle = $derived(currentLessonContentItem?.title || lessonApi.lesson?.title || 'Lesson');
|
const lessonTitle = $derived(currentLessonContentItem?.title || lessonApi.lesson?.title || 'Lesson');
|
||||||
const contentLockReason = $derived(getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson));
|
const contentLockReason = $derived(getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson));
|
||||||
const isLessonLockedForStudent = $derived($isOrgStudent === true && contentLockReason !== null);
|
|
||||||
const isStudentLessonStateReady = $derived.by(() => {
|
const isStudentLessonStateReady = $derived.by(() => {
|
||||||
if ($isOrgStudent !== true) {
|
if (!$isCourseLearnerView) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +465,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 && !isLessonLockedForStudent}
|
{#if isLiveSessionLesson && mode === MODES.view && !($isCourseLearnerView && contentLockReason)}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<LiveSessionCard
|
<LiveSessionCard
|
||||||
title={lessonTitle}
|
title={lessonTitle}
|
||||||
@@ -477,12 +476,39 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $isOrgStudent === true && !isStudentLessonStateReady}
|
{#if $isCourseLearnerView}
|
||||||
|
{#if !isStudentLessonStateReady}
|
||||||
<div class="flex justify-center py-16">
|
<div class="flex justify-center py-16">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if isLessonLockedForStudent && contentLockReason}
|
{:else if contentLockReason}
|
||||||
<StudentContentLockedNotice reason={contentLockReason} contentType={ContentType.Lesson} />
|
<StudentContentLockedNotice reason={contentLockReason} contentType={ContentType.Lesson} />
|
||||||
|
{:else if lessonApi.lesson && !isMaterialsEmpty}
|
||||||
|
{#key lessonId}
|
||||||
|
<div class="mb-20 flex w-full flex-col" in:fade={{ delay: 500 }} out:fade>
|
||||||
|
{#if !hasLessonVideos}
|
||||||
|
<LessonMaterialActions showSummarize {lessonId} alignWithNote />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each viewModeComponents as Component, index (index)}
|
||||||
|
<Component {mode} {lessonId} {courseId} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if $currentOrg.customization?.apps?.comments}
|
||||||
|
<hr class="my-2" />
|
||||||
|
|
||||||
|
<Comments {lessonId} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
<Empty
|
||||||
|
title={$t('course.navItem.lessons.materials.no_content')}
|
||||||
|
icon={VideoIcon}
|
||||||
|
variant="page"
|
||||||
|
class="text-center"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{:else if mode === MODES.edit}
|
{:else if mode === MODES.edit}
|
||||||
<UnderlineTabs.Root
|
<UnderlineTabs.Root
|
||||||
bind:value={currentTabValue}
|
bind:value={currentTabValue}
|
||||||
@@ -558,13 +584,6 @@
|
|||||||
{/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')}
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ export const isStudentExperience = derived([globalStore, isOrgStudent], ([$gs, $
|
|||||||
return $isStudent ?? false;
|
return $isStudent ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when course lesson/exercise pages should render the learner UI
|
||||||
|
* (locked notices, student empty states, no teacher authoring CTAs).
|
||||||
|
* Cloud org-site visitors always get the learner view; on the app host
|
||||||
|
* only org-role students do.
|
||||||
|
*/
|
||||||
|
export const isCourseLearnerView = derived(
|
||||||
|
[isStudentExperience, isOrgStudent],
|
||||||
|
([$isStudentExperience, $isOrgStudent]) => {
|
||||||
|
return $isStudentExperience || $isOrgStudent === true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root path for navigation: '/lms' for students, '/org/{siteName}' for admin/teacher
|
* The root path for navigation: '/lms' for students, '/org/{siteName}' for admin/teacher
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
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 { t } from '$lib/utils/functions/translations';
|
||||||
import { ContentType } from '@cio/utils/constants/content';
|
import { ContentType } from '@cio/utils/constants/content';
|
||||||
import type { CourseMember } from '$features/course/utils/types';
|
import type { CourseMember } from '$features/course/utils/types';
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
const showContentAskAiBar = $derived(
|
const showContentAskAiBar = $derived(
|
||||||
isCourseReady &&
|
isCourseReady &&
|
||||||
isLessonOrExercisePage &&
|
isLessonOrExercisePage &&
|
||||||
!($isOrgStudent && isContentLockedForStudent) &&
|
!($isCourseLearnerView && isContentLockedForStudent) &&
|
||||||
sidePanel.activePanelId !== AI_ASSISTANT_PANEL_ID
|
sidePanel.activePanelId !== AI_ASSISTANT_PANEL_ID
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user