From 8ea0ab0a7628b82a6a0a98c2f76442becbcba859 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Sun, 12 Apr 2026 18:07:33 +0100 Subject: [PATCH] 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. --- .../src/lib/features/app/init.svelte.ts | 3 +- .../components/exercise/view-mode.svelte | 7 +++-- .../components/lesson/content-list.svelte | 7 +++-- .../lesson/content-navigation-actions.svelte | 6 ++-- .../lesson/content-section-list.svelte | 7 +++-- .../lesson/lesson-page-edit-header.svelte | 6 ++-- .../components/sidebar/course-sidebar.svelte | 4 +-- .../features/course/pages/attendance.svelte | 8 +++--- .../lib/features/course/pages/exercise.svelte | 13 +++++---- .../lib/features/course/pages/lesson.svelte | 8 +++--- apps/dashboard/src/lib/utils/store/app.ts | 28 +++++++++---------- apps/dashboard/src/lib/utils/store/org.ts | 6 ---- .../dashboard/src/routes/(app)/+layout.svelte | 9 +----- .../[id]/exercises/[exerciseId]/+page.svelte | 4 +-- .../src/routes/(org-site)/+layout.svelte | 1 + apps/dashboard/src/routes/+page.svelte | 4 +-- 16 files changed, 57 insertions(+), 64 deletions(-) diff --git a/apps/dashboard/src/lib/features/app/init.svelte.ts b/apps/dashboard/src/lib/features/app/init.svelte.ts index de69ad4e..ccb0ac2a 100644 --- a/apps/dashboard/src/lib/features/app/init.svelte.ts +++ b/apps/dashboard/src/lib/features/app/init.svelte.ts @@ -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'; diff --git a/apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte b/apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte index 6de570e1..aab07e93 100644 --- a/apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte +++ b/apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte @@ -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 @@ - {:else if courseApi.course?.type === 'SELF_PACED'} + {:else if isSelfPacedLikeCourse(courseApi.course?.type)} 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); diff --git a/apps/dashboard/src/lib/features/course/components/lesson/content-section-list.svelte b/apps/dashboard/src/lib/features/course/components/lesson/content-section-list.svelte index c5fce629..f86b5883 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/content-section-list.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/content-section-list.svelte @@ -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'; diff --git a/apps/dashboard/src/lib/features/course/components/lesson/lesson-page-edit-header.svelte b/apps/dashboard/src/lib/features/course/components/lesson/lesson-page-edit-header.svelte index 39b63aef..125b913a 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/lesson-page-edit-header.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/lesson-page-edit-header.svelte @@ -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 @@
- {#if mode === MODES.edit && !$globalStore.isStudent} + {#if mode === MODES.edit && !$isOrgStudent} - {#if mode === MODES.edit && !$globalStore.isStudent} + {#if mode === MODES.edit && !$isOrgStudent}
diff --git a/apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar.svelte b/apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar.svelte index 85442f1c..a46cc031 100644 --- a/apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar.svelte +++ b/apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar.svelte @@ -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 @@ {#if isCourseReady} - + {:else}
diff --git a/apps/dashboard/src/lib/features/course/pages/attendance.svelte b/apps/dashboard/src/lib/features/course/pages/attendance.svelte index e737f94b..fb564207 100644 --- a/apps/dashboard/src/lib/features/course/pages/attendance.svelte +++ b/apps/dashboard/src/lib/features/course/pages/attendance.svelte @@ -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 @@
{ const e = { target: { checked } }; diff --git a/apps/dashboard/src/lib/features/course/pages/exercise.svelte b/apps/dashboard/src/lib/features/course/pages/exercise.svelte index f67d6032..eefef830 100644 --- a/apps/dashboard/src/lib/features/course/pages/exercise.svelte +++ b/apps/dashboard/src/lib/features/course/pages/exercise.svelte @@ -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); }); @@ -268,7 +269,7 @@
- {#if $globalStore.isStudent && courseApi.course?.id && exerciseId} + {#if $isOrgStudent && courseApi.course?.id && exerciseId} {/if} @@ -332,7 +333,7 @@ {#snippet child()}
- {#if $globalStore.isStudent} + {#if $isOrgStudent} {#if isExerciseUnlocked} {:else} diff --git a/apps/dashboard/src/lib/features/course/pages/lesson.svelte b/apps/dashboard/src/lib/features/course/pages/lesson.svelte index abff1d7c..5a976b67 100644 --- a/apps/dashboard/src/lib/features/course/pages/lesson.svelte +++ b/apps/dashboard/src/lib/features/course/pages/lesson.svelte @@ -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 @@
- {#if mode === MODES.view && $globalStore.isStudent} + {#if mode === MODES.view && $isOrgStudent} {/if} @@ -406,7 +406,7 @@ {#snippet child()}
- {#if $globalStore.isStudent && lessonApi.lesson && !isLessonUnlocked} + {#if $isOrgStudent && lessonApi.lesson && !isLessonUnlocked} - {#if !$globalStore.isStudent} + {#if !$isOrgStudent} diff --git a/apps/dashboard/src/lib/utils/store/app.ts b/apps/dashboard/src/lib/utils/store/app.ts index 3045c5f2..d3e01424 100644 --- a/apps/dashboard/src/lib/utils/store/app.ts +++ b/apps/dashboard/src/lib/utils/store/app.ts @@ -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; }); /** diff --git a/apps/dashboard/src/lib/utils/store/org.ts b/apps/dashboard/src/lib/utils/store/org.ts index d5b3f2f0..813b2d6f 100644 --- a/apps/dashboard/src/lib/utils/store/org.ts +++ b/apps/dashboard/src/lib/utils/store/org.ts @@ -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); }; diff --git a/apps/dashboard/src/routes/(app)/+layout.svelte b/apps/dashboard/src/routes/(app)/+layout.svelte index 416745aa..86fbc189 100644 --- a/apps/dashboard/src/routes/(app)/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/+layout.svelte @@ -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 })); - } - }); diff --git a/apps/dashboard/src/routes/(app)/courses/[id]/exercises/[exerciseId]/+page.svelte b/apps/dashboard/src/routes/(app)/courses/[id]/exercises/[exerciseId]/+page.svelte index 141d5e78..b439b6b9 100644 --- a/apps/dashboard/src/routes/(app)/courses/[id]/exercises/[exerciseId]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/courses/[id]/exercises/[exerciseId]/+page.svelte @@ -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; diff --git a/apps/dashboard/src/routes/(org-site)/+layout.svelte b/apps/dashboard/src/routes/(org-site)/+layout.svelte index 18923a8a..de24b60d 100644 --- a/apps/dashboard/src/routes/(org-site)/+layout.svelte +++ b/apps/dashboard/src/routes/(org-site)/+layout.svelte @@ -41,6 +41,7 @@ currentOrg.set(data.org); setTheme(data.org.theme || 'blue'); }); + {#if data.org?.isRestricted || $currentOrg.isRestricted} diff --git a/apps/dashboard/src/routes/+page.svelte b/apps/dashboard/src/routes/+page.svelte index 88e97db5..599e8146 100644 --- a/apps/dashboard/src/routes/+page.svelte +++ b/apps/dashboard/src/routes/+page.svelte @@ -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; }); +