feat(layout): Plan 2.5 P4 — WorkspaceSwitcher no-sub + SidebarNav APP_NAVIGATION
Per RFC-WS-PRIMEVUE-PLAN-2-5 §4 AD-2.5-W1 and AD-2.5-B1, §5.4 Fix 4. Changes: - WorkspaceSwitcher: sub field removed from template, WorkspaceDisplay type, and buildDisplay derivation. Stories did not carry sub args (auto-derived from seeded org.role); no WithSub story existed. New regression spec (WorkspaceSwitcher.spec.ts) locks the no-sub render. - SidebarNav: now consumes APP_NAVIGATION from src/config/navigation.ts as the single source of truth (shared with breadcrumb derivation in useNavBreadcrumb). The groups: V2NavGroup[] prop is removed; render walks top-level NavItems (branch nodes render label-heading + children; leaf nodes render as rows; items without routeName render as non-clickable dormant placeholders). Previous nav data source: groups prop fed by useV2Nav(orgNavItems) in OrganizerLayoutV2. - APP_NAVIGATION expanded with 7 entries to preserve visual sidebar continuity (Evenementen at top-level + Beheer branch with 5 children). All new entries use routeName: undefined until the corresponding v2 page lands (TODOs noted per entry); only Dashboard maps to v2-dashboard. - AppSidebar: groups prop removed; passes only :collapsed to SidebarNav. - OrganizerLayoutV2: useV2Nav(orgNavItems) plumbing retired; the layout now renders <AppSidebar /> with no nav-data wiring. - Tests: AppSidebar.spec drops the "passes groups prop to SidebarNav" assertion; OrganizerLayoutV2.spec drops the "forwards orgNavItems" assertion. New WorkspaceSwitcher no-sub regression spec (+2 tests). - Storybook: SidebarNav.stories and AppSidebar.stories updated to no longer thread navFixture/groups; WithActiveItem pushes v2-dashboard. Position of WorkspaceSwitcher (Fix 3), workspace dropdown panel (Fix 5), and AppBreadcrumb wiring (Fix 2) remain unchanged in P4 — both lands in P5. The legacy useBreadcrumb composable also remains untouched until P5 (atomic with AppTopbar refactor). Orphans flagged for follow-up cleanup (intentionally not deleted in P4): useV2Nav composable + spec, V2NavGroup/V2NavItem types, sidebarNavActive helper + spec, navFixture in stories/v2/_helpers.ts. Suite delta: 575 → 575 (+2 WorkspaceSwitcher no-sub spec, -1 AppSidebar groups-prop assertion, -1 OrganizerLayoutV2 groups-forward assertion). vue-tsc clean. Scoped ESLint clean (0 errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
import { navFixture, orgA, userFixture, withPinia } from '@/stories/v2/_helpers'
|
import { orgA, userFixture, withPinia } from '@/stories/v2/_helpers'
|
||||||
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
||||||
import { useAuthStore } from '@/stores/useAuthStore'
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
import { useShellUiStore } from '@/stores/useShellUiStore'
|
import { useShellUiStore } from '@/stores/useShellUiStore'
|
||||||
@@ -10,6 +10,9 @@ import { useShellUiStore } from '@/stores/useShellUiStore'
|
|||||||
* via WorkspaceSwitcher, useAuthStore. Each story seeds both stores on a
|
* via WorkspaceSwitcher, useAuthStore. Each story seeds both stores on a
|
||||||
* fresh Pinia. The desktop <aside> is `hidden lg:flex`, so view at a
|
* fresh Pinia. The desktop <aside> is `hidden lg:flex`, so view at a
|
||||||
* viewport ≥ 1024px to see the permanent column.
|
* viewport ≥ 1024px to see the permanent column.
|
||||||
|
*
|
||||||
|
* Plan 2.5 P4 (AD-2.5-B1): SidebarNav reads APP_NAVIGATION directly; no
|
||||||
|
* `groups` prop is threaded through AppSidebar.
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof AppSidebar> = {
|
const meta: Meta<typeof AppSidebar> = {
|
||||||
title: 'v2 Shell/AppSidebar',
|
title: 'v2 Shell/AppSidebar',
|
||||||
@@ -21,7 +24,6 @@ export default meta
|
|||||||
type Story = StoryObj<typeof AppSidebar>
|
type Story = StoryObj<typeof AppSidebar>
|
||||||
|
|
||||||
export const Expanded: Story = {
|
export const Expanded: Story = {
|
||||||
args: { groups: navFixture },
|
|
||||||
decorators: [
|
decorators: [
|
||||||
withPinia(() => {
|
withPinia(() => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -34,21 +36,17 @@ export const Expanded: Story = {
|
|||||||
shellUi.sidebarCollapsed = false
|
shellUi.sidebarCollapsed = false
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
render: args => ({
|
render: () => ({
|
||||||
components: { AppSidebar },
|
components: { AppSidebar },
|
||||||
setup() {
|
|
||||||
return { args }
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<div class="flex h-[600px]">
|
<div class="flex h-[600px]">
|
||||||
<AppSidebar :groups="args.groups" />
|
<AppSidebar />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Collapsed: Story = {
|
export const Collapsed: Story = {
|
||||||
args: { groups: navFixture },
|
|
||||||
decorators: [
|
decorators: [
|
||||||
withPinia(() => {
|
withPinia(() => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -61,14 +59,11 @@ export const Collapsed: Story = {
|
|||||||
shellUi.sidebarCollapsed = true
|
shellUi.sidebarCollapsed = true
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
render: args => ({
|
render: () => ({
|
||||||
components: { AppSidebar },
|
components: { AppSidebar },
|
||||||
setup() {
|
|
||||||
return { args }
|
|
||||||
},
|
|
||||||
template: `
|
template: `
|
||||||
<div class="flex h-[600px]">
|
<div class="flex h-[600px]">
|
||||||
<AppSidebar :groups="args.groups" />
|
<AppSidebar />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -19,8 +19,11 @@
|
|||||||
* teleported overlay. By unmounting on desktop we guarantee no modal backdrop /
|
* teleported overlay. By unmounting on desktop we guarantee no modal backdrop /
|
||||||
* focus-trapped dialog can appear on wide viewports regardless of mobileOpen state.
|
* focus-trapped dialog can appear on wide viewports regardless of mobileOpen state.
|
||||||
*
|
*
|
||||||
* nav groups arrive as a prop — this file must NOT import @/navigation directly
|
* Nav data: as of Plan 2.5 P4, SidebarNav reads APP_NAVIGATION from
|
||||||
* (components-v2 import boundary; the layout page supplies nav data).
|
* @/config/navigation directly — no prop is threaded through AppSidebar.
|
||||||
|
* The components-v2 import boundary still applies to @/navigation (the v1
|
||||||
|
* source), but @/config/navigation is the unclassified central registry
|
||||||
|
* and is consumed in-place by SidebarNav.
|
||||||
*
|
*
|
||||||
* Deliberate simplification: crewli-starter's bespoke Teleport tooltip (shown in
|
* Deliberate simplification: crewli-starter's bespoke Teleport tooltip (shown in
|
||||||
* collapsed mode for nav items) is NOT ported here. SidebarNav already provides
|
* collapsed mode for nav items) is NOT ported here. SidebarNav already provides
|
||||||
@@ -41,19 +44,10 @@ import { computed } from 'vue'
|
|||||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||||
import Drawer from 'primevue/drawer'
|
import Drawer from 'primevue/drawer'
|
||||||
import { useShellUiStore } from '@/stores/useShellUiStore'
|
import { useShellUiStore } from '@/stores/useShellUiStore'
|
||||||
import type { V2NavGroup } from '@/types/v2/nav'
|
|
||||||
import SidebarHeader from '@/components-v2/layout/SidebarHeader.vue'
|
import SidebarHeader from '@/components-v2/layout/SidebarHeader.vue'
|
||||||
import SidebarNav from '@/components-v2/layout/SidebarNav.vue'
|
import SidebarNav from '@/components-v2/layout/SidebarNav.vue'
|
||||||
import WorkspaceSwitcher from '@/components-v2/layout/WorkspaceSwitcher.vue'
|
import WorkspaceSwitcher from '@/components-v2/layout/WorkspaceSwitcher.vue'
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
/**
|
|
||||||
* Nav groups passed in from the layout — components-v2 files may NOT
|
|
||||||
* import from @/navigation; the parent layout supplies this.
|
|
||||||
*/
|
|
||||||
groups: V2NavGroup[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const shell = useShellUiStore()
|
const shell = useShellUiStore()
|
||||||
|
|
||||||
// Shared breakpoint signal — must match SidebarHeader and the CSS `lg:` boundary (1024px).
|
// Shared breakpoint signal — must match SidebarHeader and the CSS `lg:` boundary (1024px).
|
||||||
@@ -84,10 +78,7 @@ const mobileVisible = computed<boolean>({
|
|||||||
:class="shell.sidebarCollapsed ? 'w-16' : 'w-64'"
|
:class="shell.sidebarCollapsed ? 'w-16' : 'w-64'"
|
||||||
>
|
>
|
||||||
<SidebarHeader />
|
<SidebarHeader />
|
||||||
<SidebarNav
|
<SidebarNav :collapsed="shell.sidebarCollapsed" />
|
||||||
:groups="groups"
|
|
||||||
:collapsed="shell.sidebarCollapsed"
|
|
||||||
/>
|
|
||||||
<WorkspaceSwitcher :collapsed="shell.sidebarCollapsed" />
|
<WorkspaceSwitcher :collapsed="shell.sidebarCollapsed" />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -119,10 +110,7 @@ const mobileVisible = computed<boolean>({
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<SidebarHeader />
|
<SidebarHeader />
|
||||||
<SidebarNav
|
<SidebarNav :collapsed="false" />
|
||||||
:groups="groups"
|
|
||||||
:collapsed="false"
|
|
||||||
/>
|
|
||||||
<WorkspaceSwitcher :collapsed="false" />
|
<WorkspaceSwitcher :collapsed="false" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { navFixture } from '@/stories/v2/_helpers'
|
|
||||||
import SidebarNav from '@/components-v2/layout/SidebarNav.vue'
|
import SidebarNav from '@/components-v2/layout/SidebarNav.vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SidebarNav needs no Pinia, but vue-router is installed app-wide in
|
* SidebarNav now reads APP_NAVIGATION from `@/config/navigation` directly
|
||||||
* preview.ts (the component uses useRoute + RouterLink). The
|
* (AD-2.5-B1 / Plan 2.5 P4). The previous `groups` prop is gone — stories
|
||||||
* WithActiveItem story pushes the `events` route in setup so the
|
* only vary the `collapsed` axis and the active route. vue-router is
|
||||||
* Evenementen item renders in its active state.
|
* installed app-wide in preview.ts; the WithActiveItem story pushes the
|
||||||
|
* `v2-dashboard` route so the Dashboard item renders in its active state.
|
||||||
*/
|
*/
|
||||||
const meta: Meta<typeof SidebarNav> = {
|
const meta: Meta<typeof SidebarNav> = {
|
||||||
title: 'v2 Shell/SidebarNav',
|
title: 'v2 Shell/SidebarNav',
|
||||||
@@ -22,7 +22,7 @@ export default meta
|
|||||||
type Story = StoryObj<typeof SidebarNav>
|
type Story = StoryObj<typeof SidebarNav>
|
||||||
|
|
||||||
export const Expanded: Story = {
|
export const Expanded: Story = {
|
||||||
args: { groups: navFixture, collapsed: false },
|
args: { collapsed: false },
|
||||||
render: args => ({
|
render: args => ({
|
||||||
components: { SidebarNav },
|
components: { SidebarNav },
|
||||||
setup() {
|
setup() {
|
||||||
@@ -30,14 +30,14 @@ export const Expanded: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="w-64 bg-[var(--p-content-background)]">
|
<div class="w-64 bg-[var(--p-content-background)]">
|
||||||
<SidebarNav :groups="args.groups" :collapsed="args.collapsed" />
|
<SidebarNav :collapsed="args.collapsed" />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Collapsed: Story = {
|
export const Collapsed: Story = {
|
||||||
args: { groups: navFixture, collapsed: true },
|
args: { collapsed: true },
|
||||||
render: args => ({
|
render: args => ({
|
||||||
components: { SidebarNav },
|
components: { SidebarNav },
|
||||||
setup() {
|
setup() {
|
||||||
@@ -45,26 +45,26 @@ export const Collapsed: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="w-16 bg-[var(--p-content-background)]">
|
<div class="w-16 bg-[var(--p-content-background)]">
|
||||||
<SidebarNav :groups="args.groups" :collapsed="args.collapsed" />
|
<SidebarNav :collapsed="args.collapsed" />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithActiveItem: Story = {
|
export const WithActiveItem: Story = {
|
||||||
args: { groups: navFixture, collapsed: false },
|
args: { collapsed: false },
|
||||||
render: args => ({
|
render: args => ({
|
||||||
components: { SidebarNav },
|
components: { SidebarNav },
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
router.push({ name: 'events' })
|
router.push({ name: 'v2-dashboard' })
|
||||||
|
|
||||||
return { args }
|
return { args }
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="w-64 bg-[var(--p-content-background)]">
|
<div class="w-64 bg-[var(--p-content-background)]">
|
||||||
<SidebarNav :groups="args.groups" :collapsed="args.collapsed" />
|
<SidebarNav :collapsed="args.collapsed" />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,71 +1,145 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* SidebarNav — registry-driven sidebar (AD-2.5-B1 / Plan 2.5 P4).
|
||||||
|
*
|
||||||
|
* Reads `APP_NAVIGATION` directly from `@/config/navigation`. This is the
|
||||||
|
* single source of truth for sidebar items and breadcrumb derivation
|
||||||
|
* (the breadcrumb side is served by `useNavBreadcrumb` over the same
|
||||||
|
* registry). Prior to P4, this component received a `groups` prop the
|
||||||
|
* parent layout built from `@/navigation/vertical`; the prop chain
|
||||||
|
* (OrganizerLayoutV2 → AppSidebar → SidebarNav) is removed.
|
||||||
|
*
|
||||||
|
* Render contract:
|
||||||
|
* - Top-level leaf nodes (routeName set, no children) render as a row.
|
||||||
|
* - Top-level branch nodes (children set) render their label as a
|
||||||
|
* section heading followed by their children as rows.
|
||||||
|
* - Nodes without a `routeName` render as a non-clickable row
|
||||||
|
* (label-only) — dormant placeholders until their v2 page lands.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import type { RouteLocationRaw } from 'unplugin-vue-router'
|
import type { RouteLocationRaw } from 'unplugin-vue-router'
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import { isNavItemActive } from '@/components-v2/layout/sidebarNavActive'
|
import { APP_NAVIGATION, type NavItem } from '@/config/navigation'
|
||||||
import type { V2NavGroup, V2NavItem } from '@/types/v2/nav'
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
groups: V2NavGroup[]
|
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
function checkActive(item: V2NavItem): boolean {
|
function isActive(item: NavItem): boolean {
|
||||||
return isNavItemActive(item, route.name)
|
if (!item.routeName)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const current = route.name
|
||||||
|
|
||||||
|
if (typeof current !== 'string')
|
||||||
|
return false
|
||||||
|
|
||||||
|
return current === item.routeName
|
||||||
|
|| current.startsWith(`${item.routeName}-`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast V2NavItem.to (vue-router generic RouteLocationRaw) to the typed version
|
// NavItem.routeName values come from APP_NAVIGATION and are always valid
|
||||||
// expected by RouterLinkTyped. V2NavItem.to values are always named-route objects
|
// named routes from the RouteNamedMap when present — the cast to the typed
|
||||||
// from the RouteNamedMap so the cast is sound.
|
// RouterLink RouteLocationRaw is sound. Items without routeName render via
|
||||||
function itemTo(item: V2NavItem): RouteLocationRaw {
|
// the non-clickable branch (see template) so this function is never called
|
||||||
return item.to as RouteLocationRaw
|
// for them.
|
||||||
|
function itemTo(item: NavItem): RouteLocationRaw {
|
||||||
|
return { name: item.routeName } as RouteLocationRaw
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!--
|
|
||||||
.nav → flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]
|
|
||||||
.nav-group → mt-[18px] (only from the second group onward, via CSS sibling selector →
|
|
||||||
Tailwind cannot express `+ .nav-group { margin-top }` on a class without
|
|
||||||
a group-based trick; using first:mt-0 / not-first:mt-[18px] via the
|
|
||||||
:not(:first-child) pseudo equivalent — `[&:not(:first-child)]:mt-[18px]`)
|
|
||||||
.nav-label → text-[11px] font-semibold text-surface-500 dark:text-surface-400
|
|
||||||
uppercase tracking-[0.06em] px-2.5 pb-1.5 whitespace-nowrap overflow-hidden
|
|
||||||
.nav-item → flex items-center gap-3 py-[9px] px-2.5 rounded-md
|
|
||||||
text-surface-600 dark:text-surface-300 text-[13.5px] font-medium
|
|
||||||
transition-[background,color] duration-150 whitespace-nowrap relative
|
|
||||||
cursor-pointer w-full text-start min-w-0
|
|
||||||
.nav-item:hover → hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0
|
|
||||||
.nav-item.active → bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold
|
|
||||||
.nav-item.active::before → the left accent bar — inexpressible in Tailwind without a plugin
|
|
||||||
(position:absolute left:-10px, custom width:3px, height:18px,
|
|
||||||
background:primary, transform:translateY(-50%)). Using <style scoped>.
|
|
||||||
.nav-item .iconify → text-[18px] flex-shrink-0 (handled by Icon's own sizing via :size)
|
|
||||||
.count → ms-auto text-[11px] font-semibold bg-surface-100 dark:bg-surface-800
|
|
||||||
text-surface-500 dark:text-surface-400 px-1.5 py-px rounded-full
|
|
||||||
.nav-item.active .count → bg-primary-600 dark:bg-primary-500 text-white
|
|
||||||
-->
|
|
||||||
<nav class="flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]">
|
<nav class="flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]">
|
||||||
<div
|
<template
|
||||||
v-for="(group, gi) in groups"
|
v-for="topItem in APP_NAVIGATION"
|
||||||
:key="gi"
|
:key="topItem.key"
|
||||||
class="[&:not(:first-child)]:mt-[18px]"
|
|
||||||
>
|
>
|
||||||
<!-- Group label: hidden in collapsed mode -->
|
<!-- Branch node: section label + children -->
|
||||||
<div
|
<div
|
||||||
v-if="group.label && !collapsed"
|
v-if="topItem.children"
|
||||||
class="text-[11px] font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-[0.06em] px-2.5 pb-1.5 whitespace-nowrap overflow-hidden"
|
class="[&:not(:first-child)]:mt-[18px]"
|
||||||
>
|
>
|
||||||
{{ group.label }}
|
<div
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="text-[11px] font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-[0.06em] px-2.5 pb-1.5 whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ topItem.label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-for="child in topItem.children"
|
||||||
|
:key="child.key"
|
||||||
|
>
|
||||||
|
<!-- Leaf with route -->
|
||||||
|
<RouterLink
|
||||||
|
v-if="child.routeName"
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
:to="itemTo(child)"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium transition-[background,color] duration-150 whitespace-nowrap relative cursor-pointer w-full text-start min-w-0"
|
||||||
|
:class="[
|
||||||
|
collapsed ? 'justify-center px-0' : 'px-2.5',
|
||||||
|
isActive(child)
|
||||||
|
? 'nav-item-active bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold'
|
||||||
|
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0',
|
||||||
|
]"
|
||||||
|
:aria-current="isActive(child) ? 'page' : undefined"
|
||||||
|
:aria-label="child.label"
|
||||||
|
:title="collapsed ? child.label : undefined"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="child.icon"
|
||||||
|
:name="child.icon"
|
||||||
|
:size="18"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="overflow-hidden text-ellipsis min-w-0"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<!-- Leaf without route: dormant placeholder -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium whitespace-nowrap relative w-full text-start min-w-0 text-surface-400 dark:text-surface-500 cursor-not-allowed"
|
||||||
|
:class="collapsed ? 'justify-center px-0' : 'px-2.5'"
|
||||||
|
:aria-disabled="true"
|
||||||
|
:title="collapsed ? child.label : undefined"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="child.icon"
|
||||||
|
:name="child.icon"
|
||||||
|
:size="18"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="overflow-hidden text-ellipsis min-w-0"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Top-level leaf with route -->
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-for="item in group.items"
|
v-else-if="topItem.routeName"
|
||||||
:key="item.id"
|
|
||||||
v-slot="{ href, navigate }"
|
v-slot="{ href, navigate }"
|
||||||
:to="itemTo(item)"
|
:to="itemTo(topItem)"
|
||||||
custom
|
custom
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -73,44 +147,54 @@ function itemTo(item: V2NavItem): RouteLocationRaw {
|
|||||||
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium transition-[background,color] duration-150 whitespace-nowrap relative cursor-pointer w-full text-start min-w-0"
|
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium transition-[background,color] duration-150 whitespace-nowrap relative cursor-pointer w-full text-start min-w-0"
|
||||||
:class="[
|
:class="[
|
||||||
collapsed ? 'justify-center px-0' : 'px-2.5',
|
collapsed ? 'justify-center px-0' : 'px-2.5',
|
||||||
checkActive(item)
|
isActive(topItem)
|
||||||
? 'nav-item-active bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold'
|
? 'nav-item-active bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 font-semibold'
|
||||||
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0',
|
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-0',
|
||||||
]"
|
]"
|
||||||
:aria-current="checkActive(item) ? 'page' : undefined"
|
:aria-current="isActive(topItem) ? 'page' : undefined"
|
||||||
:aria-label="item.label"
|
:aria-label="topItem.label"
|
||||||
:title="collapsed ? item.label : undefined"
|
:title="collapsed ? topItem.label : undefined"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
:name="item.icon"
|
v-if="topItem.icon"
|
||||||
|
:name="topItem.icon"
|
||||||
:size="18"
|
:size="18"
|
||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Text label: hidden in collapsed mode -->
|
|
||||||
<span
|
<span
|
||||||
v-if="!collapsed"
|
v-if="!collapsed"
|
||||||
class="overflow-hidden text-ellipsis min-w-0"
|
class="overflow-hidden text-ellipsis min-w-0"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ topItem.label }}
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Count badge: hidden in collapsed mode -->
|
|
||||||
<span
|
|
||||||
v-if="item.count != null && !collapsed"
|
|
||||||
class="ms-auto text-[11px] font-semibold px-1.5 py-px rounded-full"
|
|
||||||
:class="[
|
|
||||||
checkActive(item)
|
|
||||||
? 'bg-primary-600 dark:bg-primary-500 text-white'
|
|
||||||
: 'bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ item.count }}
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
|
||||||
|
<!-- Top-level leaf without route: dormant placeholder -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center gap-3 py-[9px] rounded-md text-[13.5px] font-medium whitespace-nowrap relative w-full text-start min-w-0 text-surface-400 dark:text-surface-500 cursor-not-allowed"
|
||||||
|
:class="collapsed ? 'justify-center px-0' : 'px-2.5'"
|
||||||
|
:aria-disabled="true"
|
||||||
|
:title="collapsed ? topItem.label : undefined"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="topItem.icon"
|
||||||
|
:name="topItem.icon"
|
||||||
|
:size="18"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="overflow-hidden text-ellipsis min-w-0"
|
||||||
|
>
|
||||||
|
{{ topItem.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type { Organisation } from '@/types/auth'
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/**
|
/**
|
||||||
* When true (collapsed sidebar), hide the name/sub meta text and
|
* When true (collapsed sidebar), hide the name meta text and
|
||||||
* show only the logo square — mirrors crewli-starter's collapsed prop.
|
* show only the logo square — mirrors crewli-starter's collapsed prop.
|
||||||
*/
|
*/
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
@@ -43,8 +43,6 @@ interface WorkspaceDisplay {
|
|||||||
id: string
|
id: string
|
||||||
initials: string
|
initials: string
|
||||||
name: string
|
name: string
|
||||||
/** The role string is the relevant context identifier in Crewli. */
|
|
||||||
sub: string
|
|
||||||
gradient: [string, string]
|
gradient: [string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +58,6 @@ function buildDisplay(org: Organisation): WorkspaceDisplay {
|
|||||||
id: org.id,
|
id: org.id,
|
||||||
initials,
|
initials,
|
||||||
name: org.name,
|
name: org.name,
|
||||||
sub: org.role,
|
|
||||||
gradient: computeOrgGradient(org.id),
|
gradient: computeOrgGradient(org.id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +126,7 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
|||||||
{{ current.initials }}
|
{{ current.initials }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Meta: name + sub (hidden in collapsed mode) -->
|
<!-- Meta: name (hidden in collapsed mode) -->
|
||||||
<!-- .ws-switcher .meta: flex-1, min-w-0, flex-col, line-height, text-left -->
|
<!-- .ws-switcher .meta: flex-1, min-w-0, flex-col, line-height, text-left -->
|
||||||
<span
|
<span
|
||||||
v-if="!collapsed && current"
|
v-if="!collapsed && current"
|
||||||
@@ -139,10 +136,6 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
|||||||
<span class="truncate text-[13.5px] font-semibold text-[var(--p-text-color)]">
|
<span class="truncate text-[13.5px] font-semibold text-[var(--p-text-color)]">
|
||||||
{{ current.name }}
|
{{ current.name }}
|
||||||
</span>
|
</span>
|
||||||
<!-- .ws-switcher .meta .sub: text-[11.5px], muted, truncate -->
|
|
||||||
<span class="truncate text-[11.5px] text-[var(--p-text-muted-color)]">
|
|
||||||
{{ current.sub }}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Chevron (.ws-switcher .chev: color fg-subtle, flex-shrink-0) -->
|
<!-- Chevron (.ws-switcher .chev: color fg-subtle, flex-shrink-0) -->
|
||||||
@@ -185,12 +178,10 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
|||||||
:style="{ background: `linear-gradient(135deg, ${ws.gradient[0]}, ${ws.gradient[1]})` }"
|
:style="{ background: `linear-gradient(135deg, ${ws.gradient[0]}, ${ws.gradient[1]})` }"
|
||||||
>{{ ws.initials }}</span>
|
>{{ ws.initials }}</span>
|
||||||
|
|
||||||
<!-- Name + sub stack -->
|
<!-- Name -->
|
||||||
<span>
|
<span>
|
||||||
<!-- .pop-ws .opt .name -->
|
<!-- .pop-ws .opt .name -->
|
||||||
<div class="text-[14px] font-semibold text-[var(--p-text-color)]">{{ ws.name }}</div>
|
<div class="text-[14px] font-semibold text-[var(--p-text-color)]">{{ ws.name }}</div>
|
||||||
<!-- .pop-ws .opt .sub -->
|
|
||||||
<div class="mt-[2px] text-[12.5px] text-[var(--p-text-muted-color)]">{{ ws.sub }}</div>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Check mark for active org (.pop-ws .opt .check-mark) -->
|
<!-- Check mark for active org (.pop-ws .opt .check-mark) -->
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
* Strategy: mount with @vue/test-utils stubs for all heavy children (SidebarHeader,
|
* Strategy: mount with @vue/test-utils stubs for all heavy children (SidebarHeader,
|
||||||
* SidebarNav, WorkspaceSwitcher, Drawer) so we test only:
|
* SidebarNav, WorkspaceSwitcher, Drawer) so we test only:
|
||||||
* 1. Renders the 3 child components (SidebarHeader, SidebarNav, WorkspaceSwitcher).
|
* 1. Renders the 3 child components (SidebarHeader, SidebarNav, WorkspaceSwitcher).
|
||||||
* 2. Passes `groups` prop to SidebarNav.
|
* 2. Mobile Drawer v-model:visible wires to shell.mobileOpen (get path).
|
||||||
* 3. Mobile Drawer v-model:visible wires to shell.mobileOpen (get path).
|
* 3. Drawer close (v-model:visible = false) calls shell.setMobileOpen(false).
|
||||||
* 4. Drawer close (v-model:visible = false) calls shell.setMobileOpen(false).
|
* 4. Drawer is NOT rendered when isMobile=false (desktop); IS rendered when isMobile=true.
|
||||||
* 5. Drawer is NOT rendered when isMobile=false (desktop); IS rendered when isMobile=true.
|
* 5. Desktop <aside> applies correct width class based on sidebarCollapsed.
|
||||||
* 6. Desktop <aside> applies correct width class based on sidebarCollapsed.
|
*
|
||||||
|
* Plan 2.5 P4 (AD-2.5-B1): the `groups` prop was retired from AppSidebar/SidebarNav;
|
||||||
|
* SidebarNav now reads APP_NAVIGATION from `@/config/navigation` directly. The
|
||||||
|
* prior "passes groups prop to SidebarNav" assertion is therefore gone.
|
||||||
*
|
*
|
||||||
* Stubs: Drawer is stubbed with a simple slot passthrough so we can inspect
|
* Stubs: Drawer is stubbed with a simple slot passthrough so we can inspect
|
||||||
* whether its `visible` prop is correctly bound to the store.
|
* whether its `visible` prop is correctly bound to the store.
|
||||||
@@ -22,7 +25,6 @@ import { ref } from 'vue'
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { useShellUiStore } from '@/stores/useShellUiStore'
|
import { useShellUiStore } from '@/stores/useShellUiStore'
|
||||||
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
||||||
import type { V2NavGroup } from '@/types/v2/nav'
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mock @vueuse/core so we can control `isMobile` per test.
|
// Mock @vueuse/core so we can control `isMobile` per test.
|
||||||
@@ -42,19 +44,6 @@ vi.mock('@vueuse/core', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// A minimal nav group fixture
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const testGroups: V2NavGroup[] = [
|
|
||||||
{
|
|
||||||
label: 'Main',
|
|
||||||
items: [
|
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: 'tabler-home', to: { name: 'dashboard' } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Stubs
|
// Stubs
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -77,8 +66,8 @@ const globalStubs = {
|
|||||||
SidebarHeader: { name: 'SidebarHeader', template: '<div class="sidebar-header-stub" />' },
|
SidebarHeader: { name: 'SidebarHeader', template: '<div class="sidebar-header-stub" />' },
|
||||||
SidebarNav: {
|
SidebarNav: {
|
||||||
name: 'SidebarNav',
|
name: 'SidebarNav',
|
||||||
props: ['groups', 'collapsed'],
|
props: ['collapsed'],
|
||||||
template: '<div class="sidebar-nav-stub" :data-collapsed="collapsed" :data-groups-count="groups.length" />',
|
template: '<div class="sidebar-nav-stub" :data-collapsed="collapsed" />',
|
||||||
},
|
},
|
||||||
WorkspaceSwitcher: {
|
WorkspaceSwitcher: {
|
||||||
name: 'WorkspaceSwitcher',
|
name: 'WorkspaceSwitcher',
|
||||||
@@ -87,9 +76,8 @@ const globalStubs = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountSidebar(groups: V2NavGroup[] = testGroups) {
|
function mountSidebar() {
|
||||||
return mount(AppSidebar, {
|
return mount(AppSidebar, {
|
||||||
props: { groups },
|
|
||||||
global: {
|
global: {
|
||||||
plugins: [createPinia()],
|
plugins: [createPinia()],
|
||||||
stubs: globalStubs,
|
stubs: globalStubs,
|
||||||
@@ -127,14 +115,6 @@ describe('AppSidebar', () => {
|
|||||||
expect(wrapper.find('.workspace-switcher-stub').exists()).toBe(true)
|
expect(wrapper.find('.workspace-switcher-stub').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes groups prop to SidebarNav', () => {
|
|
||||||
const wrapper = mountSidebar()
|
|
||||||
const nav = wrapper.find('.sidebar-nav-stub')
|
|
||||||
|
|
||||||
// Our stub renders data-groups-count from the groups prop
|
|
||||||
expect(nav.attributes('data-groups-count')).toBe(String(testGroups.length))
|
|
||||||
})
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Drawer v-if: mount/unmount based on isMobile
|
// Drawer v-if: mount/unmount based on isMobile
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* WorkspaceSwitcher.spec.ts — regression coverage for AD-2.5-W1.
|
||||||
|
*
|
||||||
|
* Scope: assert that the workspace switcher no longer renders a "sub" line
|
||||||
|
* (previously the role string sourced from org.role). The full popover /
|
||||||
|
* org-switch behaviour is left to Storybook visual coverage; this file
|
||||||
|
* exists solely as the AD-2.5-W1 lock against re-introduction of the sub
|
||||||
|
* meta line in the trigger or in the popover row.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { useAuthStore } from '@/stores/useAuthStore'
|
||||||
|
import WorkspaceSwitcher from '@/components-v2/layout/WorkspaceSwitcher.vue'
|
||||||
|
import type { Organisation, User } from '@/types/auth'
|
||||||
|
|
||||||
|
const userFixture: User = {
|
||||||
|
id: 'usr_01',
|
||||||
|
first_name: 'Bert',
|
||||||
|
last_name: 'Hausmans',
|
||||||
|
full_name: 'Bert Hausmans',
|
||||||
|
date_of_birth: null,
|
||||||
|
email: 'bert@hausmans.nl',
|
||||||
|
phone: null,
|
||||||
|
timezone: 'Europe/Amsterdam',
|
||||||
|
locale: 'nl',
|
||||||
|
avatar: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgFixture: Organisation = {
|
||||||
|
id: 'org_a',
|
||||||
|
name: 'Festival Crew NL',
|
||||||
|
slug: 'festival-crew-nl',
|
||||||
|
role: 'org_admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalStubs = {
|
||||||
|
Icon: { template: '<span class="icon-stub" />' },
|
||||||
|
Popover: {
|
||||||
|
name: 'Popover',
|
||||||
|
template: '<div class="popover-stub"><slot /></div>',
|
||||||
|
methods: {
|
||||||
|
toggle() {},
|
||||||
|
hide() {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountSwitcher() {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
auth.user = userFixture
|
||||||
|
auth.organisations = [orgFixture]
|
||||||
|
|
||||||
|
return mount(WorkspaceSwitcher, {
|
||||||
|
props: { collapsed: false },
|
||||||
|
global: {
|
||||||
|
stubs: globalStubs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WorkspaceSwitcher', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render a sub line (AD-2.5-W1)', () => {
|
||||||
|
const wrapper = mountSwitcher()
|
||||||
|
|
||||||
|
// Role string used to back the sub line — must not leak into rendered output.
|
||||||
|
expect(wrapper.text()).not.toContain(orgFixture.role)
|
||||||
|
|
||||||
|
// No selector / class tied to the removed sub element.
|
||||||
|
expect(wrapper.html()).not.toMatch(/workspace-sub|ws-sub|meta-sub/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the workspace name', () => {
|
||||||
|
const wrapper = mountSwitcher()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain(orgFixture.name)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
// Future extensions (role filtering, feature flags, dynamic ordering) are
|
// Future extensions (role filtering, feature flags, dynamic ordering) are
|
||||||
// explicit follow-up RFCs — DO NOT add those fields here without one.
|
// explicit follow-up RFCs — DO NOT add those fields here without one.
|
||||||
//
|
//
|
||||||
// As of Plan 2.5 P1, only one v2 route exists (/v2/dashboard → v2-dashboard).
|
// As of Plan 2.5 P4, SidebarNav consumes this directly. Only the Dashboard
|
||||||
// As pages-v2/ grows, add an entry per route here so both the sidebar (via
|
// entry currently has a v2 route (v2-dashboard); all other entries are
|
||||||
// SidebarNav, wired in P4) and the breadcrumb (via useNavBreadcrumb) update
|
// dormant placeholders (routeName: undefined) until their respective
|
||||||
// from one place.
|
// pages-v2/<page>.vue lands. Cross-app linking to v1 routes is NOT allowed
|
||||||
|
// — dormant items render as label-only non-clickable in the sidebar.
|
||||||
|
|
||||||
export interface NavItem {
|
export interface NavItem {
|
||||||
|
|
||||||
@@ -41,4 +42,51 @@ export const APP_NAVIGATION: NavItem[] = [
|
|||||||
routeName: 'v2-dashboard',
|
routeName: 'v2-dashboard',
|
||||||
icon: 'tabler-home',
|
icon: 'tabler-home',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: assign route when v2-events page lands
|
||||||
|
{
|
||||||
|
key: 'events',
|
||||||
|
label: 'Evenementen',
|
||||||
|
icon: 'tabler-calendar-event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'beheer',
|
||||||
|
label: 'Beheer',
|
||||||
|
children: [
|
||||||
|
// TODO: assign route when v2-organisation page lands
|
||||||
|
{
|
||||||
|
key: 'organisation',
|
||||||
|
label: 'Mijn Organisatie',
|
||||||
|
icon: 'tabler-building',
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: assign route when v2-members page lands
|
||||||
|
{
|
||||||
|
key: 'members',
|
||||||
|
label: 'Leden',
|
||||||
|
icon: 'tabler-users',
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: assign route when v2-organisation-companies page lands
|
||||||
|
{
|
||||||
|
key: 'organisation-companies',
|
||||||
|
label: 'Bedrijven',
|
||||||
|
icon: 'tabler-building',
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: assign route when v2-organisation-form-failures page lands
|
||||||
|
{
|
||||||
|
key: 'organisation-form-failures',
|
||||||
|
label: 'Form failures',
|
||||||
|
icon: 'tabler-alert-triangle',
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: assign route when v2-organisation-settings page lands
|
||||||
|
{
|
||||||
|
key: 'organisation-settings',
|
||||||
|
label: 'Instellingen',
|
||||||
|
icon: 'tabler-settings',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,28 +8,24 @@
|
|||||||
* the ported PrimeVue shell pieces (AppSidebar / AppTopbar / RightDrawer)
|
* the ported PrimeVue shell pieces (AppSidebar / AppTopbar / RightDrawer)
|
||||||
* and renders routed pages via <RouterView/> in the default slot.
|
* and renders routed pages via <RouterView/> in the default slot.
|
||||||
*
|
*
|
||||||
* Nav data is sourced HERE: the layouts zone may import @/navigation,
|
* Plan 2.5 P4 (AD-2.5-B1): SidebarNav now consumes APP_NAVIGATION from
|
||||||
* whereas components-v2 may NOT (import-boundary matrix). orgNavItems is
|
* `@/config/navigation` directly. This layout no longer derives or
|
||||||
* folded into V2NavGroup[] by useV2Nav() and passed to AppSidebar :groups.
|
* passes nav data; the `useV2Nav(orgNavItems)` plumbing has been retired.
|
||||||
*
|
*
|
||||||
* No provide/inject: each shell piece reads its own state from
|
* No provide/inject: each shell piece reads its own state from
|
||||||
* useShellUiStore / useAuthStore (RFC AD-G4). This layout wires
|
* useShellUiStore / useAuthStore (RFC AD-G4). This layout wires
|
||||||
* composition + nav data only; it owns no shell state.
|
* composition only; it owns no shell state.
|
||||||
*/
|
*/
|
||||||
import { orgNavItems } from '@/navigation/vertical'
|
|
||||||
import { useV2Nav } from '@/composables/useV2Nav'
|
|
||||||
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
||||||
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
||||||
import AppTopbar from '@/components-v2/layout/AppTopbar.vue'
|
import AppTopbar from '@/components-v2/layout/AppTopbar.vue'
|
||||||
import RightDrawer from '@/components-v2/layout/RightDrawer.vue'
|
import RightDrawer from '@/components-v2/layout/RightDrawer.vue'
|
||||||
|
|
||||||
const { groups } = useV2Nav(orgNavItems)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppShellV2>
|
<AppShellV2>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<AppSidebar :groups="groups" />
|
<AppSidebar />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #topbar>
|
<template #topbar>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import OrganizerLayoutV2 from '@/layouts/OrganizerLayoutV2.vue'
|
|||||||
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
||||||
|
|
||||||
// Stub the 3 leaf shell components: this test verifies COMPOSITION
|
// Stub the 3 leaf shell components: this test verifies COMPOSITION
|
||||||
// (right component in right slot + nav data forwarded), not their
|
// (right component in right slot), not their internals (those have
|
||||||
// internals (those have their own unit tests). Stubs keep jsdom free
|
// their own unit tests). Stubs keep jsdom free of PrimeVue teleport/
|
||||||
// of PrimeVue teleport/overlay/breakpoint machinery.
|
// overlay/breakpoint machinery.
|
||||||
|
//
|
||||||
|
// Plan 2.5 P4 (AD-2.5-B1): AppSidebar no longer takes a `groups` prop
|
||||||
|
// — SidebarNav reads APP_NAVIGATION from `@/config/navigation` directly.
|
||||||
const AppSidebarStub = defineComponent({
|
const AppSidebarStub = defineComponent({
|
||||||
name: 'AppSidebarStub',
|
name: 'AppSidebarStub',
|
||||||
props: { groups: { type: Array, default: () => [] } },
|
template: '<aside data-testid="sidebar-stub" />',
|
||||||
template: '<aside data-testid="sidebar-stub" :data-groups-count="groups.length" />',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const AppTopbarStub = defineComponent({
|
const AppTopbarStub = defineComponent({
|
||||||
@@ -59,15 +61,6 @@ describe('OrganizerLayoutV2 (wired shell)', () => {
|
|||||||
expect(wrapper.find('[data-testid="page"]').exists()).toBe(true)
|
expect(wrapper.find('[data-testid="page"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('forwards orgNavItems folded into V2NavGroup[] to AppSidebar :groups', async () => {
|
|
||||||
const wrapper = await mountLayout()
|
|
||||||
const sidebar = wrapper.find('[data-testid="sidebar-stub"]')
|
|
||||||
|
|
||||||
// orgNavItems has link entries + a {heading:'Beheer'} → useV2Nav folds
|
|
||||||
// into ≥1 group. A non-zero count proves the nav chain is wired.
|
|
||||||
expect(Number(sidebar.attributes('data-groups-count'))).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('no longer renders the Plan-1 skeleton placeholders', async () => {
|
it('no longer renders the Plan-1 skeleton placeholders', async () => {
|
||||||
const wrapper = await mountLayout()
|
const wrapper = await mountLayout()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user