Refactor app state management: replace globalStore.isStudent with isOrgStudent across multiple components for improved clarity and consistency. Update related logic to utilize isSelfPacedLikeCourse for self-paced course checks.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { BaseApi, classroomio } from '$lib/utils/services/api';
|
||||
import { currentOrg, isOrgStudent, orgs } from '$lib/utils/store/org';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { currentOrg, orgs } from '$lib/utils/store/org';
|
||||
import { defaultProfileState, defaultUserState, profile, user } from '$lib/utils/store/user';
|
||||
|
||||
import type { AccountResponse } from './types';
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { ContentType } from '@cio/utils/constants/content';
|
||||
import { getOrderedNavigableContent } from '$features/course/utils/content';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { openCourseCompletionModal } from '$features/course/store/course-completion-modal';
|
||||
import { isSelfPacedLikeCourse } from '$features/course/utils/compliance-utils';
|
||||
import { toExerciseQuestionModel } from './question-type-utils';
|
||||
import { getExerciseQuestionLabels } from './question-labels';
|
||||
import axios from 'axios';
|
||||
@@ -154,7 +155,7 @@
|
||||
|
||||
const allContentItems = getOrderedNavigableContent(courseApi.course);
|
||||
const allComplete =
|
||||
$globalStore.isStudent && allContentItems.length > 0 && allContentItems.every((item) => item.isComplete);
|
||||
$isOrgStudent && allContentItems.length > 0 && allContentItems.every((item) => item.isComplete);
|
||||
|
||||
if (allComplete && courseApi.course?.id) {
|
||||
openCourseCompletionModal(courseApi.course.id);
|
||||
@@ -573,7 +574,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if courseApi.course?.type === 'SELF_PACED'}
|
||||
{:else if isSelfPacedLikeCourse(courseApi.course?.type)}
|
||||
<Badge
|
||||
variant="success"
|
||||
title={$t('course.navItem.lessons.exercises.all_exercises.view_mode.status_submitted')}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Chip } from '@cio/ui/custom/chip';
|
||||
import { lessonApi, exerciseApi, courseApi, contentApi } from '$features/course/api';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import formatDate from '$lib/utils/functions/formatDate';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { CourseContentItem } from '$features/course/utils/types';
|
||||
@@ -25,6 +25,7 @@
|
||||
import ContentRow from './content-row.svelte';
|
||||
import ContentActions from './content-actions.svelte';
|
||||
import { deleteContent, saveContent, toggleLock } from './content-action-helpers';
|
||||
import { isSelfPacedLikeCourse } from '$features/course/utils/compliance-utils';
|
||||
|
||||
const flipDurationMs = 300;
|
||||
|
||||
@@ -43,9 +44,9 @@
|
||||
let courseId = $derived(courseApi.course?.id || '');
|
||||
const profileId = $derived($profile?.id || '');
|
||||
const contentData = $derived(getCourseContent(courseApi.course));
|
||||
const isStudentView = $derived($globalStore.isStudent === true);
|
||||
const isStudentView = $derived($isOrgStudent === true);
|
||||
const isLiveCourse = $derived(courseApi.course?.type === 'LIVE_CLASS');
|
||||
const isSelfPacedCourse = $derived(courseApi.course?.type === 'SELF_PACED');
|
||||
const isSelfPacedCourse = $derived(isSelfPacedLikeCourse(courseApi.course?.type));
|
||||
const metaChipClass =
|
||||
'inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-1 text-[10px] text-gray-700 dark:bg-neutral-700 dark:text-gray-200';
|
||||
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
import { CircleCheckIcon } from '$features/ui/icons';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Tooltip from '@cio/ui/base/tooltip';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courseApi, lessonApi } from '$features/course/api';
|
||||
import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content';
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
const currentLessonItem = $derived(lessonId ? lessonItems.find((l) => l.id === lessonId) : null);
|
||||
const isLessonLocked = $derived(
|
||||
$globalStore.isStudent && currentLessonItem && !(currentLessonItem.isUnlocked ?? false)
|
||||
$isOrgStudent && currentLessonItem && !(currentLessonItem.isUnlocked ?? false)
|
||||
);
|
||||
const isMarkCompleteDisabled = $derived(isMarkingComplete || isLessonLocked || isLessonComplete);
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
updateCourseContentCompletion(currentLessonId, isComplete);
|
||||
|
||||
const allComplete =
|
||||
$globalStore.isStudent &&
|
||||
$isOrgStudent &&
|
||||
isComplete &&
|
||||
navigableContentItems.length > 0 &&
|
||||
navigableContentItems.every((item) => item.isComplete);
|
||||
|
||||
+4
-3
@@ -14,7 +14,7 @@
|
||||
import { HoverableItem, AttachmentIcon } from '@cio/ui/custom/moving-icons';
|
||||
import formatDate from '$lib/utils/functions/formatDate';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { contentApi, courseApi, exerciseApi, lessonApi } from '$features/course/api';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
@@ -27,6 +27,7 @@
|
||||
import ContentActions from './content-actions.svelte';
|
||||
import SectionHeader from './section-header.svelte';
|
||||
import { deleteContent, saveContent, toggleLock } from './content-action-helpers';
|
||||
import { isSelfPacedLikeCourse } from '$features/course/utils/compliance-utils';
|
||||
|
||||
type CrudParam = {
|
||||
sectionId?: string;
|
||||
@@ -52,9 +53,9 @@
|
||||
const courseId = $derived(courseApi.course?.id || '');
|
||||
const profileId = $derived($profile?.id || '');
|
||||
const contentData = $derived(getCourseContent(courseApi.course));
|
||||
const isStudentView = $derived($globalStore.isStudent === true);
|
||||
const isStudentView = $derived($isOrgStudent === true);
|
||||
const isLiveCourse = $derived(courseApi.course?.type === 'LIVE_CLASS');
|
||||
const isSelfPacedCourse = $derived(courseApi.course?.type === 'SELF_PACED');
|
||||
const isSelfPacedCourse = $derived(isSelfPacedLikeCourse(courseApi.course?.type));
|
||||
const metaChipClass =
|
||||
'inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-1 text-[10px] text-gray-700 dark:bg-neutral-700 dark:text-gray-200';
|
||||
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
import DeleteLessonConfirmation from '$features/course/components/lesson/delete-lesson-confirmation.svelte';
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if mode === MODES.edit && !$globalStore.isStudent}
|
||||
{#if mode === MODES.edit && !$isOrgStudent}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<InputField
|
||||
className="max-w-xl"
|
||||
@@ -55,7 +55,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if mode === MODES.edit && !$globalStore.isStudent}
|
||||
{#if mode === MODES.edit && !$isOrgStudent}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconButton onclick={onToggleLock} aria-label={lockLabel} title={lockLabel}>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { orgs } from '$lib/utils/store/org';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
import OrgLogo from '$features/ui/sidebar/org-sidebar/org-logo.svelte';
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<Sidebar.Content>
|
||||
{#if isCourseReady}
|
||||
<Navigation path={currentPath} {id} isStudent={$globalStore.isStudent} />
|
||||
<Navigation path={currentPath} {id} isStudent={$isOrgStudent ?? undefined} />
|
||||
{:else}
|
||||
<Sidebar.Group class="pt-0!">
|
||||
<div class="flex h-8 items-center gap-2 rounded-md px-2">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courseApi, attendanceApi } from '$features/course/api';
|
||||
import { getLectureNo } from '$features/course/utils/functions';
|
||||
@@ -23,7 +23,7 @@
|
||||
let { courseId }: Props = $props();
|
||||
|
||||
const students: CourseMembers = $derived(
|
||||
$globalStore.isStudent
|
||||
$isOrgStudent
|
||||
? courseApi.group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||
: courseApi.group.people.filter((person) => !!person.profile && Number(person.roleId) === ROLE.STUDENT)
|
||||
);
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
async function handleAttendanceChange(e: any, student: CourseMember, lesson: CourseContentItem) {
|
||||
if ($globalStore.isStudent) return;
|
||||
if ($isOrgStudent) return;
|
||||
|
||||
const isPresent = e.target.checked;
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<Table.Cell class="text-center">
|
||||
<div class="flex justify-center">
|
||||
<Checkbox
|
||||
disabled={$globalStore.isStudent}
|
||||
disabled={$isOrgStudent}
|
||||
checked={getAttendanceStatus(student.id, lesson.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const e = { target: { checked } };
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import { ZExerciseUpdate } from '@cio/utils/validation/exercise';
|
||||
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
|
||||
import { transformQuestionsToApiFormat } from '$features/course/components/exercise/functions';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { exerciseApi } from '$features/course/api';
|
||||
@@ -37,6 +37,7 @@
|
||||
import UpdateDescription from '$features/course/components/exercise/update-description.svelte';
|
||||
import { ContentNavigationActions } from '$features/course/components/lesson';
|
||||
import { RefreshPageData, RoleBasedSecurity } from '$features/ui';
|
||||
import { isSelfPacedLikeCourse } from '$features/course/utils/compliance-utils';
|
||||
import { getOrderedNavigableContent } from '$features/course/utils/content';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import VideoIcon from '@lucide/svelte/icons/video';
|
||||
@@ -94,7 +95,7 @@
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if ($globalStore.isStudent || !courseApi.course?.id) return;
|
||||
if ($isOrgStudent || !courseApi.course?.id) return;
|
||||
|
||||
// Transform questionnaire to API format for validation
|
||||
// Include all non-deleted options (even empty ones) so Zod can catch validation errors
|
||||
@@ -231,7 +232,7 @@
|
||||
);
|
||||
const isExerciseUnlocked = $derived(exerciseContentItem?.isUnlocked ?? false);
|
||||
|
||||
const isSelfPacedCourse = $derived(courseApi.course?.type === 'SELF_PACED');
|
||||
const isSelfPacedCourse = $derived(isSelfPacedLikeCourse(courseApi.course?.type));
|
||||
const activeQuestionTypeIds = $derived(
|
||||
($questionnaire.questions ?? []).filter((q) => !q.deletedAt).map((q) => getQuestionTypeId(q))
|
||||
);
|
||||
@@ -244,7 +245,7 @@
|
||||
isSelfPacedCourse && isExerciseFullyAutoGradable(activeQuestionTypeIds)
|
||||
);
|
||||
const teacherAutoGradeBadge = $derived.by(() => {
|
||||
if ($globalStore.isStudent || !isSelfPacedCourse || activeQuestionTypeIds.length === 0) return null;
|
||||
if ($isOrgStudent || !isSelfPacedCourse || activeQuestionTypeIds.length === 0) return null;
|
||||
return isExerciseFullyAutoGradable(activeQuestionTypeIds) ? ('auto' as const) : ('manual' as const);
|
||||
});
|
||||
</script>
|
||||
@@ -268,7 +269,7 @@
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if $globalStore.isStudent && courseApi.course?.id && exerciseId}
|
||||
{#if $isOrgStudent && courseApi.course?.id && exerciseId}
|
||||
<ContentNavigationActions courseId={courseApi.course.id} {exerciseId} />
|
||||
{/if}
|
||||
|
||||
@@ -332,7 +333,7 @@
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="overflow-x-hidden">
|
||||
{#if $globalStore.isStudent}
|
||||
{#if $isOrgStudent}
|
||||
{#if isExerciseUnlocked}
|
||||
<ViewMode {preview} {exerciseId} isFetchingExercise={isFetching} {mySubmissions} />
|
||||
{:else}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { RefreshPageData, UnsavedChanges } from '$features/ui';
|
||||
@@ -365,7 +365,7 @@
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if mode === MODES.view && $globalStore.isStudent}
|
||||
{#if mode === MODES.view && $isOrgStudent}
|
||||
<ContentNavigationActions {lessonId} {courseId} />
|
||||
{/if}
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="overflow-x-hidden lg:w-full xl:w-11/12">
|
||||
{#if $globalStore.isStudent && lessonApi.lesson && !isLessonUnlocked}
|
||||
{#if $isOrgStudent && lessonApi.lesson && !isLessonUnlocked}
|
||||
<Empty
|
||||
title={$t('course.navItem.lessons.content_locked_title')}
|
||||
description={$t('course.navItem.lessons.content_locked_description')}
|
||||
@@ -488,7 +488,7 @@
|
||||
variant="page"
|
||||
class="text-center"
|
||||
>
|
||||
{#if !$globalStore.isStudent}
|
||||
{#if !$isOrgStudent}
|
||||
<Button onclick={toggleMode}>
|
||||
{$t('course.navItem.lessons.materials.get_started')}
|
||||
</Button>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { currentOrg, currentOrgPath } from './org';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||
import { currentOrgPath } from './org';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
|
||||
export const globalStore = writable<{
|
||||
isDark: boolean;
|
||||
isOrgSite: boolean;
|
||||
orgSiteName: string;
|
||||
isStudent?: boolean;
|
||||
}>({
|
||||
isDark: false,
|
||||
isOrgSite: false,
|
||||
orgSiteName: '',
|
||||
isStudent: undefined
|
||||
orgSiteName: ''
|
||||
});
|
||||
|
||||
export const isOrgStudent = derived(currentOrg, ($currentOrg) => {
|
||||
if ($currentOrg.roleId === 0) return null;
|
||||
|
||||
return $currentOrg.roleId === ROLE.STUDENT;
|
||||
});
|
||||
|
||||
/**
|
||||
* Single source of truth: is this a student experience?
|
||||
* Cloud: orgSite is always student. Self-hosted: check role.
|
||||
*
|
||||
* why are we not using isOrgStudent from org store?
|
||||
* because this is derived from the currentOrg which returns null if no org OR boolean if org is present. Once org data is set, we then update globalStore.isStudent to the same value.
|
||||
*
|
||||
* why are we not using only globalStore.isStudent for this?
|
||||
* because in the cloud instance, when a teacher logs into the lms aka org site, they need to be treated as a student so they see what the student sees.
|
||||
* Cloud: orgSite is always student (a teacher on an org subdomain sees the student view).
|
||||
* Self-hosted: derived from isOrgStudent (role-based).
|
||||
*/
|
||||
export const isStudentExperience = derived(globalStore, ($gs) => {
|
||||
export const isStudentExperience = derived([globalStore, isOrgStudent], ([$gs, $isStudent]) => {
|
||||
const isCloud = PUBLIC_IS_SELFHOSTED !== 'true';
|
||||
if (isCloud) return $gs.isOrgSite;
|
||||
return $gs.isStudent ?? false;
|
||||
|
||||
return $isStudent ?? false;
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,12 +43,6 @@ export const isOrgAdmin = derived(currentOrg, ($currentOrg) => {
|
||||
|
||||
return $currentOrg.roleId === ROLE.ADMIN;
|
||||
});
|
||||
export const isOrgStudent = derived(currentOrg, ($currentOrg) => {
|
||||
if ($currentOrg.roleId === 0) return null;
|
||||
|
||||
return $currentOrg.roleId === ROLE.STUDENT;
|
||||
});
|
||||
|
||||
const getActivePlan = (org: AccountOrg) => {
|
||||
return org.plans.find((p) => p.isActive);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { setupCloudAnalytics } from '$lib/utils/functions/appSetup';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrg, isOrgStudent } from '$lib/utils/store/org';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { appInitApi } from '$features/app/init.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { authClient } from '$lib/utils/services/auth/client';
|
||||
@@ -98,13 +98,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($isOrgStudent === null) return;
|
||||
|
||||
if ($isOrgStudent !== $globalStore.isStudent) {
|
||||
globalStore.update((s) => ({ ...s, isStudent: !!$isOrgStudent }));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<UpgradeModal />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
clearQuestionnaireValidation,
|
||||
reset
|
||||
} from '$features/course/components/exercise/store';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { Question } from '$features/course/types';
|
||||
import {
|
||||
@@ -24,7 +24,7 @@
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
const path = `/courses/${data.courseId}/lessons`;
|
||||
const isLockedForStudent = $derived($globalStore.isStudent && data.exercise?.isUnlocked === false);
|
||||
const isLockedForStudent = $derived($isOrgStudent && data.exercise?.isUnlocked === false);
|
||||
|
||||
$effect(() => {
|
||||
if (!data.exercise || isLockedForStudent) return;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
currentOrg.set(data.org);
|
||||
setTheme(data.org.theme || 'blue');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if data.org?.isRestricted || $currentOrg.isRestricted}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
importThemeComponent,
|
||||
normalizeLandingPageSettings
|
||||
} from '$features/org/utils/landing-page';
|
||||
import { basePath } from '$lib/utils/store/app';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { basePath, globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
@@ -82,6 +81,7 @@
|
||||
const mod = await importThemeComponent(settings.theme);
|
||||
ThemeComponent = mod.default;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Reference in New Issue
Block a user