feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { default as ProgramSidebar } from './program-sidebar.svelte';
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import { BackButton } from '@cio/ui';
|
||||
import { HoverableItem, NewsFeedIcon, CourseIcon, PeopleIcon, SettingsIcon } from '@cio/ui/custom/moving-icons';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrgPath, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { isStudentExperience } from '$lib/utils/store/app';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { programApi } from '$features/program/api';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
let { path, id }: Props = $props();
|
||||
|
||||
const currentPath = $derived(path || page.url.pathname);
|
||||
const isLmsProgramContext = $derived(page.url.searchParams.get('source') === 'lms' || $isStudentExperience);
|
||||
const programsListPath = $derived(isLmsProgramContext ? '/lms/programs' : `${$currentOrgPath}/programs`);
|
||||
|
||||
const currentMemberRole = $derived(programApi.members.find((m) => m.profileId === $profile.id)?.roleId ?? null);
|
||||
|
||||
// Admin + Tutor can manage members; org admins always can
|
||||
const canManageMembers = $derived(
|
||||
$isOrgAdmin === true || (currentMemberRole !== null && currentMemberRole <= ROLE.TUTOR)
|
||||
);
|
||||
|
||||
// Only Admins and org admins can access settings
|
||||
const canManageSettings = $derived($isOrgAdmin === true || currentMemberRole === ROLE.ADMIN);
|
||||
|
||||
const navItems = $derived(
|
||||
[
|
||||
{
|
||||
id: 'newsfeed',
|
||||
title: $t('programs.sidebar.newsfeed') || 'Newsfeed',
|
||||
url: `/programs/${id}/newsfeed`,
|
||||
isActive: currentPath.endsWith('/newsfeed'),
|
||||
icon: NewsFeedIcon,
|
||||
show: () => true
|
||||
},
|
||||
{
|
||||
id: 'courses',
|
||||
title: $t('programs.sidebar.courses') || 'Courses',
|
||||
url: `/programs/${id}/courses`,
|
||||
isActive: currentPath.endsWith('/courses'),
|
||||
icon: CourseIcon,
|
||||
show: () => true
|
||||
},
|
||||
{
|
||||
id: 'people',
|
||||
title: $t('programs.sidebar.people') || 'People',
|
||||
url: `/programs/${id}/people`,
|
||||
isActive: currentPath.endsWith('/people'),
|
||||
icon: PeopleIcon,
|
||||
show: () => canManageMembers
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: $t('programs.sidebar.settings') || 'Settings',
|
||||
url: `/programs/${id}/settings`,
|
||||
isActive: currentPath.endsWith('/settings'),
|
||||
icon: SettingsIcon,
|
||||
show: () => canManageSettings
|
||||
}
|
||||
].filter((item) => item.show())
|
||||
);
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="pt-0!">
|
||||
<BackButton
|
||||
href={resolve(programsListPath, {})}
|
||||
label={$t(isLmsProgramContext ? 'lms_navigation.programs' : 'org_navigation.programs') || 'Programs'}
|
||||
class="px-2! py-2!"
|
||||
/>
|
||||
|
||||
<Sidebar.Menu>
|
||||
{#each navItems as item (item.id)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton tooltipContent={item.title} isActive={item.isActive}>
|
||||
{#snippet child({ props })}
|
||||
<HoverableItem>
|
||||
{#snippet children(isHovered)}
|
||||
{@const Icon = item.icon}
|
||||
<a href={resolve(item.url, {})} {...props}>
|
||||
<Icon {isHovered} size={16} />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</HoverableItem>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { orgs, isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
import OrgLogo from '$features/ui/sidebar/org-sidebar/org-logo.svelte';
|
||||
import Navigation from './program-sidebar-navigation.svelte';
|
||||
import SidebarSkeleton from '$features/ui/sidebar/sidebar-skeleton.svelte';
|
||||
import UpgradePoweredBy from '$features/ui/upgrade-powered-by.svelte';
|
||||
import { useSidebar } from '@cio/ui/base/sidebar';
|
||||
|
||||
const SIDEBAR_ITEM_SKELETON_COUNT = 4;
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
id: string;
|
||||
isProgramReady?: boolean;
|
||||
}
|
||||
|
||||
let { path, id, isProgramReady = true }: Props = $props();
|
||||
|
||||
const isOrgLoaded = $derived($orgs.length > 0 && $profile.id);
|
||||
const sidebar = useSidebar();
|
||||
const currentPath = $derived(path || page.url.pathname);
|
||||
</script>
|
||||
|
||||
{#if !isOrgLoaded}
|
||||
<SidebarSkeleton />
|
||||
{:else}
|
||||
<Sidebar.Root collapsible="icon">
|
||||
<Sidebar.Header>
|
||||
<OrgLogo />
|
||||
</Sidebar.Header>
|
||||
|
||||
<Sidebar.Content>
|
||||
{#if isProgramReady}
|
||||
<Navigation path={currentPath} {id} />
|
||||
{:else}
|
||||
<Sidebar.Group class="pt-0!">
|
||||
<div class="flex h-8 items-center gap-2 rounded-md px-2">
|
||||
<Skeleton class="ui:size-4 ui:rounded-md" />
|
||||
<Skeleton class="ui:h-4 ui:w-24" />
|
||||
</div>
|
||||
<Sidebar.Menu>
|
||||
{#each Array(SIDEBAR_ITEM_SKELETON_COUNT) as _, i (i)}
|
||||
<Sidebar.MenuItem>
|
||||
<div class="ui:flex ui:h-8 ui:items-center ui:gap-2 ui:rounded-md ui:px-2" data-sidebar="menu-skeleton">
|
||||
<Skeleton class="ui:size-4 ui:rounded-md" />
|
||||
<Skeleton class="ui:h-4 ui:max-w-32 ui:flex-1" />
|
||||
</div>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
{/if}
|
||||
</Sidebar.Content>
|
||||
|
||||
<Sidebar.Rail />
|
||||
|
||||
{#if $isFreePlan}
|
||||
<Sidebar.Footer>
|
||||
<UpgradePoweredBy
|
||||
class="static [&_span]:font-normal [&_span]:shadow-none"
|
||||
showOnlyLogo={!sidebar.open || sidebar.isMobile}
|
||||
/>
|
||||
</Sidebar.Footer>
|
||||
{/if}
|
||||
</Sidebar.Root>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user