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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
3ef286ed39
commit
ca3fdd5b6a
@@ -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
|
||||||
@@ -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/
|
- 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.
|
- 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
|
## Naming Convention
|
||||||
|
|
||||||
- Use kebab-case for files (e.g. `user-profile.svelte`, `org.svelte.ts`).
|
- Use kebab-case for files (e.g. `user-profile.svelte`, `org.svelte.ts`).
|
||||||
|
|||||||
+32
-11
@@ -7,8 +7,10 @@
|
|||||||
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 { get } from 'svelte/store';
|
||||||
import { isCourseLearnerView } 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 { getStudentContentLockTitleKey } from '$features/course/utils/content-lock-utils';
|
||||||
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';
|
||||||
@@ -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) {
|
function goToContent(target: CourseContentItem | null) {
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
if ($isCourseLearnerView) {
|
const lockReason = getNavigableLockReason(target);
|
||||||
const targetType = target.type === ContentType.Lesson ? ContentType.Lesson : ContentType.Exercise;
|
|
||||||
const lockReason = getStudentContentLockReason(courseApi.course, target.id, targetType);
|
|
||||||
|
|
||||||
if (lockReason) {
|
if (get(isCourseLearnerView) && lockReason) {
|
||||||
return;
|
snackbar.error(getStudentContentLockTitleKey(lockReason));
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const courseIdResolved = courseApi.course?.id;
|
const courseIdResolved = courseApi.course?.id;
|
||||||
@@ -71,6 +78,20 @@
|
|||||||
|
|
||||||
const isPrevDisabled = $derived(!prevNextContent.prev);
|
const isPrevDisabled = $derived(!prevNextContent.prev);
|
||||||
const isNextDisabled = $derived(!prevNextContent.next);
|
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(() => {
|
const isLessonComplete = $derived.by(() => {
|
||||||
if (!lessonId) return false;
|
if (!lessonId) return false;
|
||||||
const lesson = lessonItems.find((l) => l.id === lessonId);
|
const lesson = lessonItems.find((l) => l.id === lessonId);
|
||||||
@@ -145,7 +166,7 @@
|
|||||||
updateCourseContentCompletion(currentLessonId, isComplete);
|
updateCourseContentCompletion(currentLessonId, isComplete);
|
||||||
|
|
||||||
const allComplete =
|
const allComplete =
|
||||||
$isOrgStudent &&
|
get(isCourseLearnerView) &&
|
||||||
isComplete &&
|
isComplete &&
|
||||||
navigableContentItems.length > 0 &&
|
navigableContentItems.length > 0 &&
|
||||||
navigableContentItems.every((item) => item.isComplete);
|
navigableContentItems.every((item) => item.isComplete);
|
||||||
@@ -231,14 +252,14 @@
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => goToContent(prevNextContent.prev)}
|
onclick={() => goToContent(prevNextContent.prev)}
|
||||||
disabled={isPrevDisabled}
|
disabled={isPrevDisabled || isPrevNavBlocked}
|
||||||
aria-label={$t('course.navItem.lessons.prev')}
|
aria-label={$t('course.navItem.lessons.prev')}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon size={14} />
|
<ChevronLeftIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side="bottom" sideOffset={4}>
|
<Tooltip.Content side="bottom" sideOffset={4}>
|
||||||
{$t('course.navItem.lessons.prev_shortcut')}
|
{prevNavTooltip}
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
|
||||||
@@ -248,14 +269,14 @@
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => goToContent(prevNextContent.next)}
|
onclick={() => goToContent(prevNextContent.next)}
|
||||||
disabled={isNextDisabled}
|
disabled={isNextDisabled || isNextNavBlocked}
|
||||||
aria-label={$t('course.navItem.lessons.next')}
|
aria-label={$t('course.navItem.lessons.next')}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon size={14} />
|
<ChevronRightIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side="bottom" sideOffset={4}>
|
<Tooltip.Content side="bottom" sideOffset={4}>
|
||||||
{$t('course.navItem.lessons.next_shortcut')}
|
{nextNavTooltip}
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user