From ca3fdd5b6a5265717964560237acef1cbb56830a Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 16 Jul 2026 12:34:03 +0100 Subject: [PATCH] fix(dashboard): lesson nav build error and add CI build check (#765) * fix(dashboard): repair lesson nav build and improve locked feedback Replace stale $isOrgStudent reference with isCourseLearnerView to fix the Svelte build error. Disable prev/next when the adjacent item is locked, show lock tooltips, and surface a snackbar if navigation is blocked (including keyboard shortcuts). Co-authored-by: Rotimi Best * ci: add package build check and AGENTS.md verification steps Document required dashboard/API build commands for agents and add a GitHub Actions workflow that runs those builds on PRs touching apps or packages. Copy dashboard .env.example in CI so vite build has required public env vars. Co-authored-by: Rotimi Best --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- .github/workflows/package-build-check.yml | 53 +++++++++++++++++++ AGENTS.md | 23 ++++++++ .../lesson/content-navigation-actions.svelte | 43 +++++++++++---- 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/package-build-check.yml diff --git a/.github/workflows/package-build-check.yml b/.github/workflows/package-build-check.yml new file mode 100644 index 00000000..c027f9f4 --- /dev/null +++ b/.github/workflows/package-build-check.yml @@ -0,0 +1,53 @@ +name: Package build check + +on: + pull_request: + paths: + - 'apps/dashboard/**' + - 'apps/api/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - 'package.json' + - 'turbo.json' + - '.nvmrc' + - '.github/workflows/package-build-check.yml' + push: + branches: + - main + paths: + - 'apps/dashboard/**' + - 'apps/api/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - 'package.json' + - 'turbo.json' + - '.nvmrc' + - '.github/workflows/package-build-check.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Prepare dashboard env + run: cp apps/dashboard/.env.example apps/dashboard/.env + + - name: Build dashboard + run: pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build + + - name: Build API + run: pnpm --filter @cio/api^... build && pnpm --filter @cio/api build diff --git a/AGENTS.md b/AGENTS.md index 67252006..f47f2431 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,29 @@ When a task requires factual information (API specifications, context window siz - Commit messages must follow the Conventional Commits 1.0.0 spec: https://www.conventionalcommits.org/en/v1.0.0/ - Never include agent-provider attribution in commits or commit trailers. Do not add `Co-authored-by` lines for Cursor, Claude, Codex, or any other tool. +## Verification before commit/PR + +Before committing or opening/updating a PR, run the build checks that match the areas you changed. Use Node 20 (the repo's `.nvmrc` version): + +```bash +export PATH="$HOME/.nvm/versions/node/v20.19.3/bin:$PATH" +``` + +- **Dashboard changes** (`apps/dashboard/**`, or shared packages consumed by the dashboard): + + ```bash + test -f apps/dashboard/.env || cp apps/dashboard/.env.example apps/dashboard/.env + pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build + ``` + +- **API changes** (`apps/api/**`, or shared packages consumed by the API): + + ```bash + pnpm --filter @cio/api^... build && pnpm --filter @cio/api build + ``` + +Also run `pnpm format:check` (see Translation, Formatting, and Git Workflow above). Do not commit if any verification step fails. + ## Naming Convention - Use kebab-case for files (e.g. `user-profile.svelte`, `org.svelte.ts`). 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 0b6c8300..9ec965d8 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,8 +7,10 @@ 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 { get } from 'svelte/store'; import { isCourseLearnerView } from '$lib/utils/store/app'; import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar'; + import { getStudentContentLockTitleKey } from '$features/course/utils/content-lock-utils'; import { t } from '$lib/utils/functions/translations'; import { courseApi, lessonApi } from '$features/course/api'; import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content'; @@ -50,16 +52,21 @@ }; }); + function getNavigableLockReason(target: CourseContentItem | null) { + if (!target) return null; + + const targetType = target.type === ContentType.Lesson ? ContentType.Lesson : ContentType.Exercise; + return getStudentContentLockReason(courseApi.course, target.id, targetType); + } + 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); + const lockReason = getNavigableLockReason(target); - if (lockReason) { - return; - } + if (get(isCourseLearnerView) && lockReason) { + snackbar.error(getStudentContentLockTitleKey(lockReason)); + return; } const courseIdResolved = courseApi.course?.id; @@ -71,6 +78,20 @@ const isPrevDisabled = $derived(!prevNextContent.prev); const isNextDisabled = $derived(!prevNextContent.next); + const prevNavLockReason = $derived($isCourseLearnerView ? getNavigableLockReason(prevNextContent.prev) : null); + const nextNavLockReason = $derived($isCourseLearnerView ? getNavigableLockReason(prevNextContent.next) : null); + const isPrevNavBlocked = $derived(Boolean(prevNavLockReason)); + const isNextNavBlocked = $derived(Boolean(nextNavLockReason)); + const prevNavTooltip = $derived( + prevNavLockReason + ? $t(getStudentContentLockTitleKey(prevNavLockReason)) + : $t('course.navItem.lessons.prev_shortcut') + ); + const nextNavTooltip = $derived( + nextNavLockReason + ? $t(getStudentContentLockTitleKey(nextNavLockReason)) + : $t('course.navItem.lessons.next_shortcut') + ); const isLessonComplete = $derived.by(() => { if (!lessonId) return false; const lesson = lessonItems.find((l) => l.id === lessonId); @@ -145,7 +166,7 @@ updateCourseContentCompletion(currentLessonId, isComplete); const allComplete = - $isOrgStudent && + get(isCourseLearnerView) && isComplete && navigableContentItems.length > 0 && navigableContentItems.every((item) => item.isComplete); @@ -231,14 +252,14 @@ size="icon-sm" variant="outline" onclick={() => goToContent(prevNextContent.prev)} - disabled={isPrevDisabled} + disabled={isPrevDisabled || isPrevNavBlocked} aria-label={$t('course.navItem.lessons.prev')} > - {$t('course.navItem.lessons.prev_shortcut')} + {prevNavTooltip} @@ -248,14 +269,14 @@ size="icon-sm" variant="outline" onclick={() => goToContent(prevNextContent.next)} - disabled={isNextDisabled} + disabled={isNextDisabled || isNextNavBlocked} aria-label={$t('course.navItem.lessons.next')} > - {$t('course.navItem.lessons.next_shortcut')} + {nextNavTooltip}