- FIX A (IMPORTANT): PrimeVue Breadcrumb ignores `route` key; map non-last items with `command: () => router.push(item.to)` for real client-side nav - FIX B: add type="button" to all 6 native <button> chrome elements - FIX C: authStore.logout() bare call matches project no-void pattern - FIX D: document param-route edge case in toBreadcrumbItems - FIX E: regression test asserts command+push on non-last, no command on last, no `route` key on any item Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import { computed } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import type { ComputedRef } from 'vue'
|
|
import type { RouteLocationRaw } from 'vue-router'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** A single breadcrumb item. `to` is absent for the current (last) item. */
|
|
export interface BreadcrumbItem {
|
|
label: string
|
|
to?: RouteLocationRaw
|
|
}
|
|
|
|
/**
|
|
* Minimal shape we need from a matched route record.
|
|
* Using a structural type keeps the pure helper testable without
|
|
* requiring full RouteLocationMatched fixtures in unit tests.
|
|
* RouteLocationMatched is structurally assignable to this interface so
|
|
* route.matched (RouteLocationMatched[]) passes the type-checker.
|
|
*/
|
|
export interface BreadcrumbRouteRecord {
|
|
meta?: Record<string, unknown> & {
|
|
breadcrumb?: string
|
|
title?: string
|
|
}
|
|
name?: string | symbol | null
|
|
path?: string
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pure helper — testable without a live router
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Convert an array of matched route records into breadcrumb items.
|
|
*
|
|
* Rules:
|
|
* - Records without `meta.breadcrumb` OR `meta.title` are filtered out.
|
|
* - Label = `meta.breadcrumb` if set, else `meta.title`.
|
|
* - The last item is current — no `to` link.
|
|
* - All preceding items carry a `to` pointing at their `path` (or `name`
|
|
* when path is absent).
|
|
*/
|
|
export function toBreadcrumbItems(matched: readonly BreadcrumbRouteRecord[]): BreadcrumbItem[] {
|
|
const eligible = matched.filter(
|
|
r => r.meta?.breadcrumb !== undefined || r.meta?.title !== undefined,
|
|
)
|
|
|
|
return eligible.map((record, index): BreadcrumbItem => {
|
|
const label = (record.meta?.breadcrumb ?? record.meta?.title) as string
|
|
const isLast = index === eligible.length - 1
|
|
|
|
if (isLast)
|
|
return { label }
|
|
|
|
// Prefer path; fall back to name when path is absent/empty.
|
|
// NOTE: a non-last record whose `path` is a param template (e.g. `/events/:id`)
|
|
// would yield an unresolved-template `to` — acceptable in foundation scope because
|
|
// param routes are normally the last (current, no-`to`) segment.
|
|
// TODO TECH-WS-GUI-REDESIGN: resolve param paths if a non-leaf param route ever needs a crumb link
|
|
const to: RouteLocationRaw
|
|
= record.path
|
|
? record.path
|
|
: { name: record.name as string }
|
|
|
|
return { label, to }
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Composable — thin wrapper around useRoute()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface UseBreadcrumbReturn {
|
|
items: ComputedRef<BreadcrumbItem[]>
|
|
}
|
|
|
|
/**
|
|
* Composable that derives breadcrumb items from the current route's
|
|
* `matched` array. Reactive — updates automatically on navigation.
|
|
*
|
|
* Composables zone may NOT import from @/navigation — do not add
|
|
* route-level nav helpers here.
|
|
*/
|
|
export function useBreadcrumb(): UseBreadcrumbReturn {
|
|
const route = useRoute()
|
|
|
|
const items = computed<BreadcrumbItem[]>(() =>
|
|
toBreadcrumbItems(route.matched),
|
|
)
|
|
|
|
return { items }
|
|
}
|