From 967f1a93bb1d8aea363a6bd66f92b74bfcf832f0 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 20 May 2026 19:32:46 +0200 Subject: [PATCH] chore(layout): remove v2 nav-folding orphans surfaced by P4 refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P4 (Plan 2.5, AD-2.5-W1 + AD-2.5-B1) refactored SidebarNav to read APP_NAVIGATION directly, retiring the OrganizerLayoutV2 → useV2Nav → AppSidebar :groups → SidebarNav :groups props chain. Five artifacts were deliberately left in place to keep the P4 diff focused — this commit removes them. Deleted: - src/composables/useV2Nav.ts (+ spec) — v1→v2 nav fold adapter, no production consumer post-P4 - src/types/v2/nav.ts — V2NavGroup / V2NavItem types, only consumed by the deleted composables above. types/v2/ directory removed (empty) - src/components-v2/layout/sidebarNavActive.ts (+ spec) — pure helper, SidebarNav now uses inlined active check against NavItem.routeName - navFixture export + V2NavGroup import from stories/v2/_helpers.ts Also: stale "useV2Nav(orgNavItems)" reference scrubbed from OrganizerLayoutV2.vue docstring (the function no longer exists; the comment now describes the retired plumbing generically). Suite delta: 575 → 557 (−18 specs). The drop is correct — the removed specs tested deleted dead code (sidebarNavActive: 8 specs, useV2Nav: 10 specs), not contract behaviour. vue-tsc clean. Scoped ESLint clean (0 errors). Final re-grep on all deleted symbols (useV2Nav, V2NavGroup, V2NavItem, sidebarNavActive, navFixture) returns zero hits across apps/app/src/. Per zero-compromise gap 5 (delete > adapt): orphans don't stay. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../layout/__tests__/sidebarNavActive.spec.ts | 46 -------- .../components-v2/layout/sidebarNavActive.ts | 37 ------- .../composables/__tests__/useV2Nav.spec.ts | 101 ------------------ apps/app/src/composables/useV2Nav.ts | 95 ---------------- apps/app/src/layouts/OrganizerLayoutV2.vue | 2 +- apps/app/src/stories/v2/_helpers.ts | 19 ---- apps/app/src/types/v2/nav.ts | 14 --- 7 files changed, 1 insertion(+), 313 deletions(-) delete mode 100644 apps/app/src/components-v2/layout/__tests__/sidebarNavActive.spec.ts delete mode 100644 apps/app/src/components-v2/layout/sidebarNavActive.ts delete mode 100644 apps/app/src/composables/__tests__/useV2Nav.spec.ts delete mode 100644 apps/app/src/composables/useV2Nav.ts delete mode 100644 apps/app/src/types/v2/nav.ts diff --git a/apps/app/src/components-v2/layout/__tests__/sidebarNavActive.spec.ts b/apps/app/src/components-v2/layout/__tests__/sidebarNavActive.spec.ts deleted file mode 100644 index 82ca06d2..00000000 --- a/apps/app/src/components-v2/layout/__tests__/sidebarNavActive.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { isNavItemActive } from '@/components-v2/layout/sidebarNavActive' -import type { V2NavItem } from '@/types/v2/nav' - -function makeItem(name: string): V2NavItem { - return { id: name, label: name, icon: 'tabler-home', to: { name } } -} - -describe('isNavItemActive', () => { - it('returns true when the current route name exactly matches', () => { - expect(isNavItemActive(makeItem('dashboard'), 'dashboard')).toBe(true) - }) - - it('returns false when the current route name differs', () => { - expect(isNavItemActive(makeItem('dashboard'), 'events')).toBe(false) - }) - - it('returns false when currentRouteName is null', () => { - expect(isNavItemActive(makeItem('dashboard'), null)).toBe(false) - }) - - it('returns false when currentRouteName is undefined', () => { - expect(isNavItemActive(makeItem('dashboard'), undefined)).toBe(false) - }) - - it('returns true for nested routes: item name is a prefix of current route (dash-separated)', () => { - // e.g. item "organisation" should be active on route "organisation-settings" - expect(isNavItemActive(makeItem('organisation'), 'organisation-settings')).toBe(true) - }) - - it('does not match partial-word prefixes: "org" should NOT match "organisation-settings"', () => { - expect(isNavItemActive(makeItem('org'), 'organisation-settings')).toBe(false) - }) - - it('handles symbol route names: returns false (not string — no match)', () => { - const sym = Symbol('dashboard') - - expect(isNavItemActive(makeItem('dashboard'), sym)).toBe(false) - }) - - it('item with to as { name: string } matches correctly', () => { - const item: V2NavItem = { id: 'events', label: 'Events', icon: 'tabler-calendar', to: { name: 'events' } } - - expect(isNavItemActive(item, 'events')).toBe(true) - }) -}) diff --git a/apps/app/src/components-v2/layout/sidebarNavActive.ts b/apps/app/src/components-v2/layout/sidebarNavActive.ts deleted file mode 100644 index 9305bacc..00000000 --- a/apps/app/src/components-v2/layout/sidebarNavActive.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { V2NavItem } from '@/types/v2/nav' - -/** - * Pure helper — determines whether a V2NavItem is "active" given the current - * route name. - * - * Active rules (simplest correct definition): - * 1. Exact match: currentRouteName === item.to.name - * 2. Prefix match for nested routes: currentRouteName starts with - * item.to.name + '-' (e.g. item "organisation" is active on - * "organisation-settings"). The dash boundary prevents "org" from - * spuriously matching "organisation-settings". - * - * Only `to` values that are a plain object with a string `name` property are - * compared — string/array `to` values always return false (router-push - * style not used in v1 nav). - */ -export function isNavItemActive( - item: V2NavItem, - currentRouteName: string | symbol | null | undefined, -): boolean { - if (typeof currentRouteName !== 'string') - return false - - const to = item.to - - if (typeof to !== 'object' || to === null || Array.isArray(to)) - return false - - const itemName = (to as { name?: unknown }).name - - if (typeof itemName !== 'string') - return false - - return currentRouteName === itemName - || currentRouteName.startsWith(`${itemName}-`) -} diff --git a/apps/app/src/composables/__tests__/useV2Nav.spec.ts b/apps/app/src/composables/__tests__/useV2Nav.spec.ts deleted file mode 100644 index 81e74c6e..00000000 --- a/apps/app/src/composables/__tests__/useV2Nav.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { toV2NavGroups, useV2Nav } from '@/composables/useV2Nav' -import type { V2NavItem } from '@/types/v2/nav' - -// Hand-built fixture — does NOT depend on the real orgNavItems contents. -const fixture = [ - { title: 'Alpha', to: { name: 'alpha' }, icon: { icon: 'tabler-home' } }, - { title: 'Beta', to: { name: 'beta' }, icon: { icon: 'tabler-bell' } }, - { heading: 'Group One' }, - { title: 'Gamma', to: { name: 'gamma' }, icon: { icon: 'tabler-star' }, count: 5 }, - { heading: 'Group Two' }, - { title: 'Delta', to: { name: 'delta' }, icon: { icon: 'tabler-settings' } }, -] as const - -describe('toV2NavGroups', () => { - it('places items before the first heading into a leading group with label ""', () => { - const groups = toV2NavGroups(fixture) - - expect(groups[0].label).toBe('') - expect(groups[0].items).toHaveLength(2) - }) - - it('starts a new group when a heading entry is encountered', () => { - const groups = toV2NavGroups(fixture) - - expect(groups).toHaveLength(3) - expect(groups[1].label).toBe('Group One') - expect(groups[2].label).toBe('Group Two') - }) - - it('maps item fields correctly: id, label, icon, to', () => { - const groups = toV2NavGroups(fixture) - const alpha = groups[0].items[0] as V2NavItem - - expect(alpha.id).toBe('alpha') - expect(alpha.label).toBe('Alpha') - expect(alpha.icon).toBe('tabler-home') - expect(alpha.to).toEqual({ name: 'alpha' }) - }) - - it('passes count through when present', () => { - const groups = toV2NavGroups(fixture) - const gamma = groups[1].items[0] as V2NavItem - - expect(gamma.count).toBe(5) - }) - - it('leaves count undefined when absent', () => { - const groups = toV2NavGroups(fixture) - const alpha = groups[0].items[0] as V2NavItem - - expect(alpha.count).toBeUndefined() - }) - - it('id equals the v1 route name (already kebab-case; no normalisation applied)', () => { - const groups = toV2NavGroups(fixture) - const delta = groups[2].items[0] as V2NavItem - - expect(delta.id).toBe('delta') - }) - - it('returns empty groups array for an empty input', () => { - expect(toV2NavGroups([])).toEqual([]) - }) - - it('items-only input (no headings) returns a single leading group', () => { - const onlyItems = [ - { title: 'A', to: { name: 'a' }, icon: { icon: 'tabler-a' } }, - ] as const - - const groups = toV2NavGroups(onlyItems) - - expect(groups).toHaveLength(1) - expect(groups[0].label).toBe('') - expect(groups[0].items).toHaveLength(1) - }) -}) - -describe('useV2Nav', () => { - it('returns a computed whose .value equals toV2NavGroups(items)', () => { - const { groups } = useV2Nav(fixture) - - expect(groups.value).toEqual(toV2NavGroups(fixture)) - }) - - it('consecutive headings produce an empty-items group then the next group', () => { - const consecutiveHeadings = [ - { heading: 'First' }, - { heading: 'Second' }, - { title: 'Alpha', to: { name: 'alpha' }, icon: { icon: 'tabler-home' } }, - ] as const - - const groups = toV2NavGroups(consecutiveHeadings) - - expect(groups).toHaveLength(2) - expect(groups[0].label).toBe('First') - expect(groups[0].items).toHaveLength(0) - expect(groups[1].label).toBe('Second') - expect(groups[1].items).toHaveLength(1) - }) -}) diff --git a/apps/app/src/composables/useV2Nav.ts b/apps/app/src/composables/useV2Nav.ts deleted file mode 100644 index 3d398c5e..00000000 --- a/apps/app/src/composables/useV2Nav.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { computed } from 'vue' -import type { ComputedRef } from 'vue' -import type { V2NavGroup, V2NavItem } from '@/types/v2/nav' - -// --------------------------------------------------------------------------- -// Local discriminated union for v1 nav entries (no `any`) -// --------------------------------------------------------------------------- - -interface V1NavHeading { - heading: string -} - -interface V1NavLink { - title: string - to: { name: string } - icon: { icon: string } - count?: number -} - -export type V1NavEntry = V1NavHeading | V1NavLink - -function isHeading(entry: V1NavEntry): entry is V1NavHeading { - return 'heading' in entry -} - -// --------------------------------------------------------------------------- -// Pure adapter — exported so it can be unit-tested without mounting -// --------------------------------------------------------------------------- - -/** - * Folds a flat v1 nav array into V2NavGroup[]. - * - * - A `{ heading }` entry closes the current group and opens a new one. - * - Items before the first heading are placed in a leading group with label ''. - * - A pure function with no side-effects. - */ -export function toV2NavGroups(items: readonly V1NavEntry[]): V2NavGroup[] { - if (items.length === 0) - return [] - - const groups: V2NavGroup[] = [] - let current: V2NavGroup | null = null - - for (const entry of items) { - if (isHeading(entry)) { - // Close current group (if any) and start a new named group. - if (current !== null) - groups.push(current) - - current = { label: entry.heading, items: [] } - } - else { - // Ensure there is a current group (leading ungrouped section). - if (current === null) - current = { label: '', items: [] } - - const navItem: V2NavItem = { - id: entry.to.name, // v1 route names are already kebab-case; no normalisation needed - label: entry.title, - icon: entry.icon.icon, - to: { name: entry.to.name }, - ...(entry.count !== undefined ? { count: entry.count } : {}), - } - - current.items.push(navItem) - } - } - - // Flush the last open group. - if (current !== null) - groups.push(current) - - return groups -} - -// --------------------------------------------------------------------------- -// Composable -// --------------------------------------------------------------------------- - -/** - * Wraps toV2NavGroups in a computed ref. - * - * Accepts the raw v1 nav items as a parameter so the composable (composables - * boundary zone) does not need to import from @/navigation (navigation zone). - * Call-sites in layouts/pages — which ARE allowed to import navigation — - * pass orgNavItems directly: - * - * import { orgNavItems } from '@/navigation/vertical' - * const { groups } = useV2Nav(orgNavItems) - */ -export function useV2Nav(items: readonly V1NavEntry[]): { groups: ComputedRef } { - const groups = computed(() => toV2NavGroups(items)) - - return { groups } -} diff --git a/apps/app/src/layouts/OrganizerLayoutV2.vue b/apps/app/src/layouts/OrganizerLayoutV2.vue index 713dc302..daa5ba9e 100644 --- a/apps/app/src/layouts/OrganizerLayoutV2.vue +++ b/apps/app/src/layouts/OrganizerLayoutV2.vue @@ -10,7 +10,7 @@ * * Plan 2.5 P4 (AD-2.5-B1): SidebarNav now consumes APP_NAVIGATION from * `@/config/navigation` directly. This layout no longer derives or - * passes nav data; the `useV2Nav(orgNavItems)` plumbing has been retired. + * passes nav data; the prior v1→v2 nav-fold plumbing has been retired. * * No provide/inject: each shell piece reads its own state from * useShellUiStore / useAuthStore (RFC AD-G4). This layout wires diff --git a/apps/app/src/stories/v2/_helpers.ts b/apps/app/src/stories/v2/_helpers.ts index 9ec23eb1..8cedbab2 100644 --- a/apps/app/src/stories/v2/_helpers.ts +++ b/apps/app/src/stories/v2/_helpers.ts @@ -1,7 +1,6 @@ import { createPinia, setActivePinia } from 'pinia' import type { Decorator } from '@storybook/vue3-vite' import type { Organisation, User } from '@/types/auth' -import type { V2NavGroup } from '@/types/v2/nav' /** * Fresh Pinia per story + optional seeding. The seed fn runs AFTER @@ -64,21 +63,3 @@ export const orgC: Organisation = { /** Convenience single-org list for store seeds. */ export const orgFixture: Organisation[] = [orgA] - -export const navFixture: V2NavGroup[] = [ - { - label: '', - items: [ - { id: 'dashboard', label: 'Dashboard', icon: 'tabler-smart-home', to: { name: 'dashboard' } }, - { id: 'events', label: 'Evenementen', icon: 'tabler-calendar-event', to: { name: 'events' }, count: 3 }, - ], - }, - { - label: 'Beheer', - items: [ - { id: 'organisation', label: 'Mijn Organisatie', icon: 'tabler-building', to: { name: 'organisation' } }, - { id: 'members', label: 'Leden', icon: 'tabler-users', to: { name: 'members' } }, - { id: 'organisation-settings', label: 'Instellingen', icon: 'tabler-settings', to: { name: 'organisation-settings' } }, - ], - }, -] diff --git a/apps/app/src/types/v2/nav.ts b/apps/app/src/types/v2/nav.ts deleted file mode 100644 index 18c9b4a3..00000000 --- a/apps/app/src/types/v2/nav.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { RouteLocationRaw } from 'vue-router' - -export interface V2NavItem { - id: string - label: string - icon: string // e.g. 'tabler-smart-home' - to: RouteLocationRaw - count?: number -} - -export interface V2NavGroup { - label: string // '' for an ungrouped leading section - items: V2NavItem[] -}