@quazardous/qdadm 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/package.json +5 -4
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppLayout.vue +8 -58
- package/src/components/layout/NavlinksGroup.vue +153 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +4 -58
- package/src/composables/useNavContext.ts +13 -4
- package/src/composables/useSemanticBreadcrumb.ts +7 -2
- package/src/kernel/Kernel.routing.ts +6 -12
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**Vue 3 admin framework. PrimeVue. Zero boilerplate.**
|
|
4
4
|
|
|
5
|
-
Quick start: [
|
|
5
|
+
Quick start: [README](https://github.com/quazardous/qdadm/blob/main/README.md) (5 min tutorial)
|
|
6
6
|
|
|
7
|
-
Concepts & patterns: [QDADM_CREDO
|
|
7
|
+
Concepts & patterns: [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md)
|
|
8
8
|
|
|
9
|
-
Changelog: [
|
|
9
|
+
Changelog: [CHANGELOG](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -95,20 +95,20 @@ Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSideP
|
|
|
95
95
|
|
|
96
96
|
| Doc | Purpose |
|
|
97
97
|
|-----|---------|
|
|
98
|
-
| [QDADM_CREDO](QDADM_CREDO.md) | Philosophy, patterns, concepts |
|
|
99
|
-
| [Page compositions](
|
|
100
|
-
| [CRUD Pages](
|
|
101
|
-
| [Navigation](
|
|
102
|
-
| [Architecture](
|
|
103
|
-
| [Extension](
|
|
104
|
-
| [Hooks](
|
|
105
|
-
| [Signals](
|
|
106
|
-
| [Zones](
|
|
107
|
-
| [Security](
|
|
108
|
-
| [i18n](
|
|
109
|
-
| [Codegen](
|
|
110
|
-
| [Deferred](
|
|
111
|
-
| [AGENT_GUIDE](
|
|
98
|
+
| [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md) | Philosophy, patterns, concepts |
|
|
99
|
+
| [Page compositions](https://github.com/quazardous/qdadm/blob/main/docs/page-compositions.md) | **Start here** — which composition for which need |
|
|
100
|
+
| [CRUD Pages](https://github.com/quazardous/qdadm/blob/main/docs/crud.md) | List / form / show / child pages reference |
|
|
101
|
+
| [Navigation](https://github.com/quazardous/qdadm/blob/main/docs/navigation.md) | Breadcrumb, sibling navlinks, View↔Edit toggle |
|
|
102
|
+
| [Architecture](https://github.com/quazardous/qdadm/blob/main/docs/architecture.md) | PAC pattern, layers |
|
|
103
|
+
| [Extension](https://github.com/quazardous/qdadm/blob/main/docs/extension.md) | Decorators, bundles, multi-storage |
|
|
104
|
+
| [Hooks](https://github.com/quazardous/qdadm/blob/main/docs/hooks.md) | Alter / invoke registry |
|
|
105
|
+
| [Signals](https://github.com/quazardous/qdadm/blob/main/docs/signals.md) | Event bus, SSE bridge |
|
|
106
|
+
| [Zones](https://github.com/quazardous/qdadm/blob/main/docs/zones.md) | Extensible UI blocks |
|
|
107
|
+
| [Security](https://github.com/quazardous/qdadm/blob/main/docs/security.md) | Permissions, roles |
|
|
108
|
+
| [i18n](https://github.com/quazardous/qdadm/blob/main/docs/i18n.md) | Messages, providers, strategies |
|
|
109
|
+
| [Codegen](https://github.com/quazardous/qdadm/blob/main/docs/gen.md) | Generate managers (OpenAPI / manual) |
|
|
110
|
+
| [Deferred](https://github.com/quazardous/qdadm/blob/main/docs/deferred.md) | Deferred values & warmup |
|
|
111
|
+
| [AGENT_GUIDE](https://github.com/quazardous/qdadm/blob/main/docs/AGENT_GUIDE.md) | Quick index for AI agents |
|
|
112
112
|
|
|
113
113
|
## Peer Dependencies
|
|
114
114
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/quazardous/qdadm.git"
|
|
9
|
+
"url": "git+https://github.com/quazardous/qdadm.git",
|
|
10
|
+
"directory": "packages/qdadm"
|
|
10
11
|
},
|
|
11
12
|
"homepage": "https://github.com/quazardous/qdadm#readme",
|
|
12
13
|
"bugs": {
|
|
@@ -53,11 +54,11 @@
|
|
|
53
54
|
"yaml": "^2.8.4"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
|
-
"pinia": "^2.0.0 || ^3.0.0",
|
|
57
|
+
"pinia": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
57
58
|
"primevue": "^4.0.0",
|
|
58
59
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
59
60
|
"vue": "^3.3.0",
|
|
60
|
-
"vue-router": "^4.0.0"
|
|
61
|
+
"vue-router": "^4.0.0 || ^5.0.0"
|
|
61
62
|
},
|
|
62
63
|
"peerDependenciesMeta": {
|
|
63
64
|
"vanilla-jsoneditor": {
|
package/src/components/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { default as Breadcrumb } from './layout/Breadcrumb.vue'
|
|
|
11
11
|
export { default as PageNav } from './layout/PageNav.vue'
|
|
12
12
|
export { default as Zone } from './layout/Zone.vue'
|
|
13
13
|
export { default as SidebarBox } from './layout/SidebarBox.vue'
|
|
14
|
+
export { default as NavlinksGroup } from './layout/NavlinksGroup.vue'
|
|
14
15
|
|
|
15
16
|
// Default zone components
|
|
16
17
|
export { default as DefaultHeader } from './layout/defaults/DefaultHeader.vue'
|
|
@@ -25,6 +25,7 @@ import Breadcrumb from 'primevue/breadcrumb'
|
|
|
25
25
|
import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
|
|
26
26
|
import SidebarBox from './SidebarBox.vue'
|
|
27
27
|
import Zone from './Zone.vue'
|
|
28
|
+
import NavlinksGroup from './NavlinksGroup.vue'
|
|
28
29
|
import qdadmLogo from '../../assets/logo.svg'
|
|
29
30
|
import { version as qdadmVersion } from '../../../package.json'
|
|
30
31
|
|
|
@@ -58,6 +59,9 @@ const guardDialog = useGuardDialog()
|
|
|
58
59
|
const router = useRouter()
|
|
59
60
|
const route = useRoute()
|
|
60
61
|
const app = useApp()
|
|
62
|
+
// Top-level binding so the template unwraps the ComputedRef — `app.version`
|
|
63
|
+
// nested in a plain object stays a ref object (always truthy) in v-if (#1382)
|
|
64
|
+
const appVersion = app.version
|
|
61
65
|
const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
|
|
62
66
|
const { user, logout, authEnabled } = useAuth()
|
|
63
67
|
const signals = inject<SignalBus | null>('qdadmSignals', null)
|
|
@@ -261,12 +265,6 @@ const showBreadcrumb = computed<boolean>(() => {
|
|
|
261
265
|
return true
|
|
262
266
|
})
|
|
263
267
|
|
|
264
|
-
// View↔Edit mode links (#1332/#1341/#1353) — same opt-in contract as
|
|
265
|
-
// DefaultBreadcrumb; single opposite-mode link on item pages, the parent's
|
|
266
|
-
// View/Edit pair on child pages
|
|
267
|
-
const shownModeLinks = computed(() =>
|
|
268
|
-
features.breadcrumbModeToggle === true ? modeLinks.value : []
|
|
269
|
-
)
|
|
270
268
|
</script>
|
|
271
269
|
|
|
272
270
|
<template>
|
|
@@ -293,7 +291,7 @@ const shownModeLinks = computed(() =>
|
|
|
293
291
|
<img v-if="app.logo" :src="app.logo" :alt="app.name" class="sidebar-logo" />
|
|
294
292
|
<h1 v-else>{{ app.name }}</h1>
|
|
295
293
|
</div>
|
|
296
|
-
<span v-if="
|
|
294
|
+
<span v-if="appVersion" class="version">v{{ appVersion }}</span>
|
|
297
295
|
</div>
|
|
298
296
|
|
|
299
297
|
<nav class="sidebar-nav">
|
|
@@ -392,30 +390,8 @@ const shownModeLinks = computed(() =>
|
|
|
392
390
|
</Breadcrumb>
|
|
393
391
|
|
|
394
392
|
<!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
|
|
395
|
-
links (#1332/#1341/#1353) —
|
|
396
|
-
|
|
397
|
-
<div v-if="navlinks.length > 0 || shownModeLinks.length > 0" class="layout-navlinks">
|
|
398
|
-
<!-- mode links lead the group (#j7udkh), sibling tabs follow -->
|
|
399
|
-
<template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
|
|
400
|
-
<span v-if="index > 0" class="layout-navlinks-separator">|</span>
|
|
401
|
-
<RouterLink
|
|
402
|
-
:to="mode.to"
|
|
403
|
-
class="layout-navlink breadcrumb-mode-toggle"
|
|
404
|
-
>
|
|
405
|
-
{{ mode.label }}
|
|
406
|
-
</RouterLink>
|
|
407
|
-
</template>
|
|
408
|
-
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
409
|
-
<span v-if="shownModeLinks.length > 0 || index > 0" class="layout-navlinks-separator">|</span>
|
|
410
|
-
<RouterLink
|
|
411
|
-
:to="link.to"
|
|
412
|
-
class="layout-navlink"
|
|
413
|
-
:class="{ 'layout-navlink--active': link.active }"
|
|
414
|
-
>
|
|
415
|
-
{{ link.label }}
|
|
416
|
-
</RouterLink>
|
|
417
|
-
</template>
|
|
418
|
-
</div>
|
|
393
|
+
links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
|
|
394
|
+
<NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="layout" />
|
|
419
395
|
</div>
|
|
420
396
|
|
|
421
397
|
<div class="page-content">
|
|
@@ -723,33 +699,7 @@ const shownModeLinks = computed(() =>
|
|
|
723
699
|
padding-bottom: 0;
|
|
724
700
|
}
|
|
725
701
|
|
|
726
|
-
.
|
|
727
|
-
display: flex;
|
|
728
|
-
align-items: center;
|
|
729
|
-
gap: 0.375rem;
|
|
730
|
-
font-size: var(--fad-font-size-sm);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
.layout-navlinks-separator {
|
|
734
|
-
color: var(--p-surface-400, #94a3b8);
|
|
735
|
-
opacity: 0.5;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
.layout-navlink {
|
|
739
|
-
color: var(--p-primary-500, #3b82f6);
|
|
740
|
-
text-decoration: none;
|
|
741
|
-
transition: color var(--fad-transition-fast);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
.layout-navlink:hover {
|
|
745
|
-
color: var(--p-primary-600, #2563eb);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
.layout-navlink--active {
|
|
749
|
-
color: var(--p-surface-600, #475569);
|
|
750
|
-
font-weight: var(--fad-font-weight-medium);
|
|
751
|
-
pointer-events: none;
|
|
752
|
-
}
|
|
702
|
+
/* Navlinks styles moved to NavlinksGroup.vue (#1357) */
|
|
753
703
|
|
|
754
704
|
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
755
705
|
.layout-nav-bar :deep(.p-breadcrumb) {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* NavlinksGroup - the right-side navigation group of a breadcrumb bar (#1357)
|
|
4
|
+
*
|
|
5
|
+
* Single rendering of the contract previously copy-pasted between AppLayout's
|
|
6
|
+
* inline breadcrumb and DefaultBreadcrumb: View↔Edit mode links LEAD the
|
|
7
|
+
* group (#1332/#1341/#1353, order #j7udkh), sibling navlinks follow, pipe
|
|
8
|
+
* separators between every pair, plain text (no icons).
|
|
9
|
+
*
|
|
10
|
+
* Owns the two behaviors both call sites need:
|
|
11
|
+
* - the `qdadmFeatures.breadcrumbModeToggle` opt-in gate on mode links
|
|
12
|
+
* - dedup (#1357): a navlink whose target route is already covered by a
|
|
13
|
+
* shown mode link is dropped — e.g. the auto "Details" link resolves to
|
|
14
|
+
* the parent's edit route and would render twice ("Details" + "Edit").
|
|
15
|
+
* With the feature off, no mode links are shown and every navlink stays
|
|
16
|
+
* (back-compat).
|
|
17
|
+
*
|
|
18
|
+
* `variant` keeps the historical CSS hooks of each call site so consumer
|
|
19
|
+
* overrides keep working: 'layout' = AppLayout classes, 'breadcrumb' =
|
|
20
|
+
* DefaultBreadcrumb classes.
|
|
21
|
+
*/
|
|
22
|
+
import { computed, inject } from 'vue'
|
|
23
|
+
import { RouterLink } from 'vue-router'
|
|
24
|
+
import type { NavLinkItem, BreadcrumbModeToggle } from '../../composables/useNavContext'
|
|
25
|
+
|
|
26
|
+
interface FeaturesConfig {
|
|
27
|
+
breadcrumbModeToggle?: boolean
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
navlinks?: NavLinkItem[]
|
|
34
|
+
modeLinks?: BreadcrumbModeToggle[]
|
|
35
|
+
variant?: 'layout' | 'breadcrumb'
|
|
36
|
+
}>(),
|
|
37
|
+
{
|
|
38
|
+
navlinks: () => [],
|
|
39
|
+
modeLinks: () => [],
|
|
40
|
+
variant: 'breadcrumb',
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const features = inject<FeaturesConfig>('qdadmFeatures', {})
|
|
45
|
+
|
|
46
|
+
// Opt-in (#1332): mode links render only when the feature flag is on
|
|
47
|
+
const shownModeLinks = computed<BreadcrumbModeToggle[]>(() =>
|
|
48
|
+
features.breadcrumbModeToggle === true ? props.modeLinks : []
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Dedup (#1357): drop navlinks targeting a route a mode link already covers
|
|
52
|
+
const shownNavlinks = computed<NavLinkItem[]>(() => {
|
|
53
|
+
if (shownModeLinks.value.length === 0) return props.navlinks
|
|
54
|
+
const covered = new Set(shownModeLinks.value.map((m) => m.to.name))
|
|
55
|
+
return props.navlinks.filter((link) => !link.to?.name || !covered.has(link.to.name))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const cls = computed(() =>
|
|
59
|
+
props.variant === 'layout'
|
|
60
|
+
? {
|
|
61
|
+
container: 'layout-navlinks',
|
|
62
|
+
separator: 'layout-navlinks-separator',
|
|
63
|
+
link: 'layout-navlink',
|
|
64
|
+
active: 'layout-navlink--active',
|
|
65
|
+
}
|
|
66
|
+
: {
|
|
67
|
+
container: 'breadcrumb-navlinks',
|
|
68
|
+
separator: 'navlinks-separator',
|
|
69
|
+
link: 'navlink',
|
|
70
|
+
active: 'navlink--active',
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<!-- Plain navigation; the form page's own route-leave guard covers
|
|
77
|
+
unsaved changes on edit→show -->
|
|
78
|
+
<div v-if="shownNavlinks.length > 0 || shownModeLinks.length > 0" :class="cls.container">
|
|
79
|
+
<!-- mode links lead the group (#j7udkh), sibling tabs follow -->
|
|
80
|
+
<template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
|
|
81
|
+
<span v-if="index > 0" :class="cls.separator">|</span>
|
|
82
|
+
<RouterLink :to="mode.to" class="breadcrumb-mode-toggle" :class="cls.link">
|
|
83
|
+
{{ mode.label }}
|
|
84
|
+
</RouterLink>
|
|
85
|
+
</template>
|
|
86
|
+
<template v-for="(link, index) in shownNavlinks" :key="link.to?.name || index">
|
|
87
|
+
<span v-if="shownModeLinks.length > 0 || index > 0" :class="cls.separator">|</span>
|
|
88
|
+
<RouterLink :to="link.to" :class="[cls.link, { [cls.active]: link.active }]">
|
|
89
|
+
{{ link.label }}
|
|
90
|
+
</RouterLink>
|
|
91
|
+
</template>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
/* Historical styles of each call site, keyed by variant container so the
|
|
97
|
+
look of both bars is preserved exactly (AppLayout was compact). */
|
|
98
|
+
.layout-navlinks {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 0.375rem;
|
|
102
|
+
font-size: var(--fad-font-size-sm);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.layout-navlinks-separator {
|
|
106
|
+
color: var(--p-surface-400, #94a3b8);
|
|
107
|
+
opacity: 0.5;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.layout-navlink {
|
|
111
|
+
color: var(--p-primary-500, #3b82f6);
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
transition: color var(--fad-transition-fast);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.layout-navlink:hover {
|
|
117
|
+
color: var(--p-primary-600, #2563eb);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.layout-navlink--active {
|
|
121
|
+
color: var(--p-surface-600, #475569);
|
|
122
|
+
font-weight: var(--fad-font-weight-medium);
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.breadcrumb-navlinks {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 0.5rem;
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.navlinks-separator {
|
|
134
|
+
color: var(--p-surface-400, #94a3b8);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.navlink {
|
|
138
|
+
color: var(--p-primary-500, #3b82f6);
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
transition: color 0.15s;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.navlink:hover {
|
|
144
|
+
color: var(--p-primary-700, #1d4ed8);
|
|
145
|
+
text-decoration: underline;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.navlink--active {
|
|
149
|
+
color: var(--p-surface-700, #334155);
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -12,11 +12,10 @@ import { computed, inject, ref, type Ref } from 'vue'
|
|
|
12
12
|
import { RouterLink, useRoute } from 'vue-router'
|
|
13
13
|
import { useNavContext, type BreadcrumbItem, type NavLinkItem } from '../../../composables/useNavContext'
|
|
14
14
|
import Breadcrumb from 'primevue/breadcrumb'
|
|
15
|
+
import NavlinksGroup from '../NavlinksGroup.vue'
|
|
15
16
|
|
|
16
17
|
interface FeaturesConfig {
|
|
17
18
|
breadcrumb?: boolean
|
|
18
|
-
/** Opt-in (#1332): render a View↔Edit toggle next to the terminal crumb */
|
|
19
|
-
breadcrumbModeToggle?: boolean
|
|
20
19
|
[key: string]: unknown
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -42,11 +41,6 @@ const showBreadcrumb = computed<boolean>(() => {
|
|
|
42
41
|
return true
|
|
43
42
|
})
|
|
44
43
|
|
|
45
|
-
// View↔Edit mode links (#1332/#1353) — opt-in; single opposite-mode link on
|
|
46
|
-
// item pages, the parent's View/Edit pair on child pages
|
|
47
|
-
const shownModeLinks = computed(() =>
|
|
48
|
-
features.breadcrumbModeToggle === true ? modeLinks.value : []
|
|
49
|
-
)
|
|
50
44
|
</script>
|
|
51
45
|
|
|
52
46
|
<template>
|
|
@@ -65,30 +59,8 @@ const shownModeLinks = computed(() =>
|
|
|
65
59
|
</Breadcrumb>
|
|
66
60
|
|
|
67
61
|
<!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
|
|
68
|
-
links (#1332/#1341/#1353) —
|
|
69
|
-
|
|
70
|
-
<div v-if="navlinks.length > 0 || shownModeLinks.length > 0" class="breadcrumb-navlinks">
|
|
71
|
-
<!-- mode links lead the group (#j7udkh), sibling tabs follow -->
|
|
72
|
-
<template v-for="(mode, index) in shownModeLinks" :key="`mode-${mode.target}`">
|
|
73
|
-
<span v-if="index > 0" class="navlinks-separator">|</span>
|
|
74
|
-
<RouterLink
|
|
75
|
-
:to="mode.to"
|
|
76
|
-
class="navlink breadcrumb-mode-toggle"
|
|
77
|
-
>
|
|
78
|
-
{{ mode.label }}
|
|
79
|
-
</RouterLink>
|
|
80
|
-
</template>
|
|
81
|
-
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
82
|
-
<span v-if="shownModeLinks.length > 0 || index > 0" class="navlinks-separator">|</span>
|
|
83
|
-
<RouterLink
|
|
84
|
-
:to="link.to"
|
|
85
|
-
class="navlink"
|
|
86
|
-
:class="{ 'navlink--active': link.active }"
|
|
87
|
-
>
|
|
88
|
-
{{ link.label }}
|
|
89
|
-
</RouterLink>
|
|
90
|
-
</template>
|
|
91
|
-
</div>
|
|
62
|
+
links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
|
|
63
|
+
<NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="breadcrumb" />
|
|
92
64
|
</div>
|
|
93
65
|
</template>
|
|
94
66
|
|
|
@@ -105,33 +77,7 @@ const shownModeLinks = computed(() =>
|
|
|
105
77
|
padding-bottom: 0;
|
|
106
78
|
}
|
|
107
79
|
|
|
108
|
-
.
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
gap: 0.5rem;
|
|
112
|
-
font-size: 0.875rem;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.navlinks-separator {
|
|
116
|
-
color: var(--p-surface-400, #94a3b8);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.navlink {
|
|
120
|
-
color: var(--p-primary-500, #3b82f6);
|
|
121
|
-
text-decoration: none;
|
|
122
|
-
transition: color 0.15s;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.navlink:hover {
|
|
126
|
-
color: var(--p-primary-700, #1d4ed8);
|
|
127
|
-
text-decoration: underline;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.navlink--active {
|
|
131
|
-
color: var(--p-surface-700, #334155);
|
|
132
|
-
font-weight: 500;
|
|
133
|
-
pointer-events: none;
|
|
134
|
-
}
|
|
80
|
+
/* Navlinks styles moved to NavlinksGroup.vue (#1357) */
|
|
135
81
|
|
|
136
82
|
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
137
83
|
.default-breadcrumb :deep(.p-breadcrumb) {
|
|
@@ -377,6 +377,8 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
377
377
|
})
|
|
378
378
|
|
|
379
379
|
const navlinks = computed(() => {
|
|
380
|
+
void i18nLocale.value // "Details" label re-resolves on locale change
|
|
381
|
+
|
|
380
382
|
if (!parentConfig.value) return []
|
|
381
383
|
|
|
382
384
|
const { entity: parentEntity, param, itemRoute } = parentConfig.value
|
|
@@ -391,7 +393,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
391
393
|
// Details link - use manager's idField for param name
|
|
392
394
|
const links: NavLinkItem[] = [
|
|
393
395
|
{
|
|
394
|
-
label: 'Details',
|
|
396
|
+
label: breadcrumbLabel('details', 'Details'),
|
|
395
397
|
to: {
|
|
396
398
|
name: parentRouteName!,
|
|
397
399
|
params: { [parentManager.idField!]: parentId.value as string },
|
|
@@ -448,10 +450,17 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
448
450
|
// MODE TOGGLE / MODE LINKS (#1332, #1353)
|
|
449
451
|
// ============================================================================
|
|
450
452
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
+
/** Resolve a `breadcrumb.*` label from the i18n catalog, with fallback. */
|
|
454
|
+
function breadcrumbLabel(key: string, fallback: string): string {
|
|
455
|
+
const trace = kernelI18n?.resolve(`breadcrumb.${key}`)
|
|
453
456
|
if (trace?.hit) return trace.result
|
|
454
|
-
return
|
|
457
|
+
return fallback
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function toggleLabel(target: 'show' | 'edit'): string {
|
|
461
|
+
return target === 'edit'
|
|
462
|
+
? breadcrumbLabel('edit', 'Edit')
|
|
463
|
+
: breadcrumbLabel('view', 'View')
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
/** Build one mode link if its route exists and (for edit) permission allows. */
|
|
@@ -89,10 +89,15 @@ export function computeSemanticBreadcrumb(
|
|
|
89
89
|
items.push({ kind: actionKind, entity: lastEntity, id: pendingId })
|
|
90
90
|
pendingId = null
|
|
91
91
|
} else {
|
|
92
|
-
// No pending ID
|
|
92
|
+
// No pending ID and the last item is an id-less entity level
|
|
93
|
+
// (e.g. /books/create): append the action as its own item so the
|
|
94
|
+
// entity-list crumb survives — mutating the list item into
|
|
95
|
+
// entity-create ate the "Books" level (#1356). When the last item
|
|
96
|
+
// already carries an id (/books/1/edit) the action was consumed
|
|
97
|
+
// at the id segment; swallow it as before.
|
|
93
98
|
const lastItem = items[items.length - 1]
|
|
94
99
|
if (lastItem && lastItem.kind.startsWith('entity-') && !lastItem.id) {
|
|
95
|
-
|
|
100
|
+
items.push({ kind: actionKind, entity: lastEntity })
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
continue
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
createWebHashHistory,
|
|
5
5
|
type RouteRecordRaw,
|
|
6
6
|
type RouteLocationNormalized,
|
|
7
|
-
type NavigationGuardNext,
|
|
8
7
|
type RouteRecordNormalized,
|
|
9
8
|
} from 'vue-router'
|
|
10
9
|
import { getRoutes } from '../module/moduleRegistry'
|
|
@@ -170,7 +169,9 @@ export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
170
169
|
)
|
|
171
170
|
})
|
|
172
171
|
|
|
173
|
-
|
|
172
|
+
// Return-style guard (no next() callback): supported since vue-router 4,
|
|
173
|
+
// required to avoid the R0025 deprecation warning on vue-router 5 (#1384)
|
|
174
|
+
this.router!.beforeEach((to: RouteLocationNormalized) => {
|
|
174
175
|
if (authAdapter.isAuthenticated()) {
|
|
175
176
|
wasEverAuthenticated = true
|
|
176
177
|
}
|
|
@@ -181,7 +182,6 @@ export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
181
182
|
)
|
|
182
183
|
|
|
183
184
|
if (isPublic) {
|
|
184
|
-
next()
|
|
185
185
|
return
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -204,11 +204,9 @@ export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
204
204
|
wasEverAuthenticated = false
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
return this.router!.hasRoute('login')
|
|
208
208
|
? { name: 'login', query: { session_lost: '1' } }
|
|
209
209
|
: '/'
|
|
210
|
-
next(loginRoute)
|
|
211
|
-
return
|
|
212
210
|
}
|
|
213
211
|
|
|
214
212
|
const entity = to.meta?.entity as string | undefined
|
|
@@ -233,20 +231,16 @@ export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
233
231
|
entity,
|
|
234
232
|
manager,
|
|
235
233
|
})
|
|
236
|
-
|
|
237
|
-
return
|
|
234
|
+
return { path: '/' }
|
|
238
235
|
}
|
|
239
236
|
} catch (err) {
|
|
240
237
|
console.error(
|
|
241
238
|
`[qdadm] Access check failed for ${to.path} (entity: ${entity}) — denying navigation:`,
|
|
242
239
|
err
|
|
243
240
|
)
|
|
244
|
-
|
|
245
|
-
return
|
|
241
|
+
return { path: '/' }
|
|
246
242
|
}
|
|
247
243
|
}
|
|
248
|
-
|
|
249
|
-
next()
|
|
250
244
|
})
|
|
251
245
|
}
|
|
252
246
|
|