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>
This commit is contained in:
2026-05-16 21:07:57 +02:00
parent 4f1fb7385b
commit 615a114f33
3 changed files with 108 additions and 12 deletions

View File

@@ -37,6 +37,7 @@ import Menu from 'primevue/menu'
import OverlayBadge from 'primevue/overlaybadge'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { MenuItem } from 'primevue/menuitem'
import Icon from '@/components/Icon.vue'
import { useBreadcrumb } from '@/composables/useBreadcrumb'
@@ -45,11 +46,12 @@ import { useShellUiStore } from '@/stores/useShellUiStore'
import { computeOrgGradient } from '@/utils/v2/gradient'
// ---------------------------------------------------------------------------
// Stores
// Stores / router
// ---------------------------------------------------------------------------
const shell = useShellUiStore()
const authStore = useAuthStore()
const router = useRouter()
// ---------------------------------------------------------------------------
// Breadcrumb — route-driven via useBreadcrumb()
@@ -59,14 +61,27 @@ const { items: breadcrumbItems } = useBreadcrumb()
/**
* Map BreadcrumbItem[] → PrimeVue MenuItem[].
* Non-last items get a `route` for router-link rendering.
* Last item (current) has no route/command — plain label only.
*
* The installed PrimeVue Breadcrumb (BreadcrumbItem.vue) renders
* `<a :href="item.url || '#'">` and calls `item.command` on click.
* It does NOT honour a `route` key — router-link is never invoked.
*
* Fix: non-last items navigate via `command: () => router.push(item.to)` (client-side,
* no full reload, no href="#"). Last/current item has no `to` from useBreadcrumb()
* → no command → non-interactive.
*/
const breadcrumbModel = computed<MenuItem[]>(() =>
breadcrumbItems.value.map(item => ({
label: item.label,
...(item.to !== undefined ? { route: item.to } : {}),
})),
breadcrumbItems.value.map(item => {
const base: MenuItem = { label: item.label }
if (item.to !== undefined) {
base.command = () => {
router.push(item.to!)
}
}
return base
}),
)
// ---------------------------------------------------------------------------
@@ -214,7 +229,9 @@ const userMenuItems = computed<MenuItem[]>(() => [
{
label: 'Sign out',
icon: 'tabler-logout',
command: () => authStore.logout(),
command: () => {
authStore.logout()
},
},
],
},
@@ -235,6 +252,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
.icon-btn: 38x38, rounded, transparent bg, fg-muted color, hover bg-hover
-->
<button
type="button"
class="lg:hidden inline-flex h-[38px] w-[38px] items-center justify-center rounded-[var(--p-border-radius)] border-0 bg-transparent text-[var(--p-text-muted-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-1"
aria-label="Open menu"
@click="shell.setMobileOpen(true)"
@@ -295,6 +313,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
-->
<button
v-if="authStore.currentOrganisation"
type="button"
class="ws-mobile-btn-shadow lg:hidden inline-flex h-[38px] w-[38px] flex-shrink-0 items-center justify-center rounded-[var(--p-border-radius)] border-0 text-[12px] font-bold text-white"
:style="{
background: `linear-gradient(135deg, ${mobileOrgGradient[0]}, ${mobileOrgGradient[1]})`,
@@ -332,6 +351,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
.icon-btn: 38x38, inline-flex, centered, rounded, transparent, fg-muted, hover bg-hover
-->
<button
type="button"
class="inline-flex h-[38px] w-[38px] items-center justify-center rounded-[var(--p-border-radius)] border-0 bg-transparent text-[var(--p-text-muted-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-1"
:aria-label="densityAriaLabel"
@click="toggleDensity"
@@ -344,6 +364,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
<!-- Theme toggle. -->
<button
type="button"
class="inline-flex h-[38px] w-[38px] items-center justify-center rounded-[var(--p-border-radius)] border-0 bg-transparent text-[var(--p-text-muted-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-1"
:aria-label="themeAriaLabel"
@click="toggleTheme"
@@ -364,6 +385,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
severity="danger"
>
<button
type="button"
class="inline-flex h-[38px] w-[38px] items-center justify-center rounded-[var(--p-border-radius)] border-0 bg-transparent text-[var(--p-text-muted-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-1"
aria-label="Notifications"
@click="toggleNotifPopover"