Files
crewli/apps/app/src/composables/useBreadcrumb.ts
bert.hausmans 615a114f33 fix(gui-v2): breadcrumb navigation via router.push + button type + void logout
- 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>
2026-05-16 21:07:57 +02:00

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