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 } }