chore(layout): remove v2 nav-folding orphans surfaced by P4 refactor
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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}-`)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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<V2NavGroup[]> } {
|
||||
const groups = computed(() => toV2NavGroups(items))
|
||||
|
||||
return { groups }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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' } },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
Reference in New Issue
Block a user