@quazardous/qdadm 2.7.3 → 2.9.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 +3 -2
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppLayout.vue +5 -56
- package/src/components/layout/NavlinksGroup.vue +153 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +5 -58
- package/src/composables/useNavContext.ts +61 -20
- package/src/composables/useSemanticBreadcrumb.ts +7 -2
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.9.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": {
|
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
|
|
|
@@ -241,7 +242,7 @@ watch(() => route.fullPath, () => {
|
|
|
241
242
|
|
|
242
243
|
// Navigation context (breadcrumb + navlinks from route config)
|
|
243
244
|
// Entity data comes from activeStack (populated by useEntityItemPage/useEntityItemFormPage)
|
|
244
|
-
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks,
|
|
245
|
+
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks, modeLinks } = useNavContext()
|
|
245
246
|
|
|
246
247
|
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
247
248
|
const breadcrumbOverride = ref<BreadcrumbItem[] | null>(null)
|
|
@@ -261,11 +262,6 @@ const showBreadcrumb = computed<boolean>(() => {
|
|
|
261
262
|
return true
|
|
262
263
|
})
|
|
263
264
|
|
|
264
|
-
// View↔Edit toggle on the terminal crumb (#1332/#1341) — same opt-in contract
|
|
265
|
-
// as DefaultBreadcrumb; this inline breadcrumb must honor the flag too
|
|
266
|
-
const shownModeToggle = computed(() =>
|
|
267
|
-
features.breadcrumbModeToggle === true ? modeToggle.value : null
|
|
268
|
-
)
|
|
269
265
|
</script>
|
|
270
266
|
|
|
271
267
|
<template>
|
|
@@ -391,29 +387,8 @@ const shownModeToggle = computed(() =>
|
|
|
391
387
|
</Breadcrumb>
|
|
392
388
|
|
|
393
389
|
<!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
<div v-if="navlinks.length > 0 || shownModeToggle" class="layout-navlinks">
|
|
397
|
-
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
398
|
-
<span v-if="index > 0" class="layout-navlinks-separator">|</span>
|
|
399
|
-
<RouterLink
|
|
400
|
-
:to="link.to"
|
|
401
|
-
class="layout-navlink"
|
|
402
|
-
:class="{ 'layout-navlink--active': link.active }"
|
|
403
|
-
>
|
|
404
|
-
{{ link.label }}
|
|
405
|
-
</RouterLink>
|
|
406
|
-
</template>
|
|
407
|
-
<template v-if="shownModeToggle">
|
|
408
|
-
<span v-if="navlinks.length > 0" class="layout-navlinks-separator">|</span>
|
|
409
|
-
<RouterLink
|
|
410
|
-
:to="shownModeToggle.to"
|
|
411
|
-
class="layout-navlink breadcrumb-mode-toggle"
|
|
412
|
-
>
|
|
413
|
-
{{ shownModeToggle.label }}
|
|
414
|
-
</RouterLink>
|
|
415
|
-
</template>
|
|
416
|
-
</div>
|
|
390
|
+
links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
|
|
391
|
+
<NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="layout" />
|
|
417
392
|
</div>
|
|
418
393
|
|
|
419
394
|
<div class="page-content">
|
|
@@ -721,33 +696,7 @@ const shownModeToggle = computed(() =>
|
|
|
721
696
|
padding-bottom: 0;
|
|
722
697
|
}
|
|
723
698
|
|
|
724
|
-
.
|
|
725
|
-
display: flex;
|
|
726
|
-
align-items: center;
|
|
727
|
-
gap: 0.375rem;
|
|
728
|
-
font-size: var(--fad-font-size-sm);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
.layout-navlinks-separator {
|
|
732
|
-
color: var(--p-surface-400, #94a3b8);
|
|
733
|
-
opacity: 0.5;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
.layout-navlink {
|
|
737
|
-
color: var(--p-primary-500, #3b82f6);
|
|
738
|
-
text-decoration: none;
|
|
739
|
-
transition: color var(--fad-transition-fast);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
.layout-navlink:hover {
|
|
743
|
-
color: var(--p-primary-600, #2563eb);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
.layout-navlink--active {
|
|
747
|
-
color: var(--p-surface-600, #475569);
|
|
748
|
-
font-weight: var(--fad-font-weight-medium);
|
|
749
|
-
pointer-events: none;
|
|
750
|
-
}
|
|
699
|
+
/* Navlinks styles moved to NavlinksGroup.vue (#1357) */
|
|
751
700
|
|
|
752
701
|
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
753
702
|
.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
|
|
|
@@ -24,7 +23,7 @@ const route = useRoute()
|
|
|
24
23
|
const features = inject<FeaturesConfig>('qdadmFeatures', { breadcrumb: true })
|
|
25
24
|
|
|
26
25
|
// Navigation context (breadcrumb + navlinks from route config)
|
|
27
|
-
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks,
|
|
26
|
+
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks, modeLinks } = useNavContext()
|
|
28
27
|
|
|
29
28
|
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
30
29
|
const breadcrumbOverride = inject<Ref<BreadcrumbItem[] | null>>('qdadmBreadcrumbOverride', ref(null))
|
|
@@ -42,11 +41,6 @@ const showBreadcrumb = computed<boolean>(() => {
|
|
|
42
41
|
return true
|
|
43
42
|
})
|
|
44
43
|
|
|
45
|
-
// View↔Edit toggle on the terminal crumb (#1332) — opt-in, and only when
|
|
46
|
-
// useNavContext resolved a reachable, permitted twin-mode route
|
|
47
|
-
const shownModeToggle = computed(() =>
|
|
48
|
-
features.breadcrumbModeToggle === true ? modeToggle.value : null
|
|
49
|
-
)
|
|
50
44
|
</script>
|
|
51
45
|
|
|
52
46
|
<template>
|
|
@@ -65,29 +59,8 @@ const shownModeToggle = computed(() =>
|
|
|
65
59
|
</Breadcrumb>
|
|
66
60
|
|
|
67
61
|
<!-- Navlinks (provided by PageNav for child routes) + View↔Edit mode
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div v-if="navlinks.length > 0 || shownModeToggle" class="breadcrumb-navlinks">
|
|
71
|
-
<template v-for="(link, index) in navlinks" :key="link.to?.name || index">
|
|
72
|
-
<span v-if="index > 0" class="navlinks-separator">|</span>
|
|
73
|
-
<RouterLink
|
|
74
|
-
:to="link.to"
|
|
75
|
-
class="navlink"
|
|
76
|
-
:class="{ 'navlink--active': link.active }"
|
|
77
|
-
>
|
|
78
|
-
{{ link.label }}
|
|
79
|
-
</RouterLink>
|
|
80
|
-
</template>
|
|
81
|
-
<template v-if="shownModeToggle">
|
|
82
|
-
<span v-if="navlinks.length > 0" class="navlinks-separator">|</span>
|
|
83
|
-
<RouterLink
|
|
84
|
-
:to="shownModeToggle.to"
|
|
85
|
-
class="navlink breadcrumb-mode-toggle"
|
|
86
|
-
>
|
|
87
|
-
{{ shownModeToggle.label }}
|
|
88
|
-
</RouterLink>
|
|
89
|
-
</template>
|
|
90
|
-
</div>
|
|
62
|
+
links (#1332/#1341/#1353) — shared rendering + dedup (#1357) -->
|
|
63
|
+
<NavlinksGroup :navlinks="navlinks" :mode-links="modeLinks" variant="breadcrumb" />
|
|
91
64
|
</div>
|
|
92
65
|
</template>
|
|
93
66
|
|
|
@@ -104,33 +77,7 @@ const shownModeToggle = computed(() =>
|
|
|
104
77
|
padding-bottom: 0;
|
|
105
78
|
}
|
|
106
79
|
|
|
107
|
-
.
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
gap: 0.5rem;
|
|
111
|
-
font-size: 0.875rem;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.navlinks-separator {
|
|
115
|
-
color: var(--p-surface-400, #94a3b8);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.navlink {
|
|
119
|
-
color: var(--p-primary-500, #3b82f6);
|
|
120
|
-
text-decoration: none;
|
|
121
|
-
transition: color 0.15s;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.navlink:hover {
|
|
125
|
-
color: var(--p-primary-700, #1d4ed8);
|
|
126
|
-
text-decoration: underline;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.navlink--active {
|
|
130
|
-
color: var(--p-surface-700, #334155);
|
|
131
|
-
font-weight: 500;
|
|
132
|
-
pointer-events: none;
|
|
133
|
-
}
|
|
80
|
+
/* Navlinks styles moved to NavlinksGroup.vue (#1357) */
|
|
134
81
|
|
|
135
82
|
/* Override PrimeVue Breadcrumb styles for flat look */
|
|
136
83
|
.default-breadcrumb :deep(.p-breadcrumb) {
|
|
@@ -76,9 +76,10 @@ export interface BreadcrumbItem {
|
|
|
76
76
|
* permissions are honored once the item is loaded.
|
|
77
77
|
*/
|
|
78
78
|
export interface BreadcrumbModeToggle {
|
|
79
|
-
/** Mode of the current route
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
/** Mode of the current route — absent on child pages (#1353), where the
|
|
80
|
+
* page is in neither mode of the parent item */
|
|
81
|
+
current?: 'show' | 'edit'
|
|
82
|
+
/** Mode the link navigates to */
|
|
82
83
|
target: 'show' | 'edit'
|
|
83
84
|
/** Twin-route location — a plain router.push; any dirty-form protection
|
|
84
85
|
* comes from the form page's own onBeforeRouteLeave guard */
|
|
@@ -130,6 +131,7 @@ export interface UseNavContextReturn {
|
|
|
130
131
|
breadcrumb: ComputedRef<BreadcrumbItem[]>
|
|
131
132
|
navlinks: ComputedRef<NavLinkItem[]>
|
|
132
133
|
modeToggle: ComputedRef<BreadcrumbModeToggle | null>
|
|
134
|
+
modeLinks: ComputedRef<BreadcrumbModeToggle[]>
|
|
133
135
|
|
|
134
136
|
// Helpers
|
|
135
137
|
parentConfig: ComputedRef<ParentConfig | undefined>
|
|
@@ -375,6 +377,8 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
375
377
|
})
|
|
376
378
|
|
|
377
379
|
const navlinks = computed(() => {
|
|
380
|
+
void i18nLocale.value // "Details" label re-resolves on locale change
|
|
381
|
+
|
|
378
382
|
if (!parentConfig.value) return []
|
|
379
383
|
|
|
380
384
|
const { entity: parentEntity, param, itemRoute } = parentConfig.value
|
|
@@ -389,7 +393,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
389
393
|
// Details link - use manager's idField for param name
|
|
390
394
|
const links: NavLinkItem[] = [
|
|
391
395
|
{
|
|
392
|
-
label: 'Details',
|
|
396
|
+
label: breadcrumbLabel('details', 'Details'),
|
|
393
397
|
to: {
|
|
394
398
|
name: parentRouteName!,
|
|
395
399
|
params: { [parentManager.idField!]: parentId.value as string },
|
|
@@ -443,15 +447,42 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
443
447
|
})
|
|
444
448
|
|
|
445
449
|
// ============================================================================
|
|
446
|
-
// MODE TOGGLE (#1332)
|
|
450
|
+
// MODE TOGGLE / MODE LINKS (#1332, #1353)
|
|
447
451
|
// ============================================================================
|
|
448
452
|
|
|
449
|
-
|
|
450
|
-
|
|
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}`)
|
|
451
456
|
if (trace?.hit) return trace.result
|
|
452
|
-
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')
|
|
453
464
|
}
|
|
454
465
|
|
|
466
|
+
/** Build one mode link if its route exists and (for edit) permission allows. */
|
|
467
|
+
function buildModeLink(
|
|
468
|
+
manager: EntityManager,
|
|
469
|
+
target: 'show' | 'edit',
|
|
470
|
+
id: unknown,
|
|
471
|
+
permissionEntity: unknown
|
|
472
|
+
): BreadcrumbModeToggle | null {
|
|
473
|
+
const routeName = `${manager.routePrefix}-${target}`
|
|
474
|
+
if (!router.hasRoute(routeName)) return null
|
|
475
|
+
if (target === 'edit' && !manager.canUpdate(permissionEntity ?? undefined)) return null
|
|
476
|
+
return {
|
|
477
|
+
target,
|
|
478
|
+
to: { name: routeName, params: { [manager.idField || 'id']: id as string } },
|
|
479
|
+
label: toggleLabel(target),
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Item pages (#1332): the terminal is entity-show/entity-edit — offer the
|
|
484
|
+
// OPPOSITE mode (getDefaultItemRoute's edit-preference is a landing
|
|
485
|
+
// default, not a toggle).
|
|
455
486
|
const modeToggle = computed<BreadcrumbModeToggle | null>(() => {
|
|
456
487
|
void i18nLocale.value // label re-resolves on locale change
|
|
457
488
|
|
|
@@ -464,20 +495,29 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
464
495
|
if (!manager?.routePrefix) return null
|
|
465
496
|
|
|
466
497
|
const current = terminal.kind === 'entity-edit' ? 'edit' : 'show'
|
|
467
|
-
// Target is the OPPOSITE of the current mode — getDefaultItemRoute's
|
|
468
|
-
// edit-preference is a landing default, not a toggle.
|
|
469
498
|
const target = current === 'edit' ? 'show' : 'edit'
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (target === 'edit' && !manager.canUpdate(entityData.value ?? undefined)) return null
|
|
499
|
+
const link = buildModeLink(manager, target, terminal.id, entityData.value)
|
|
500
|
+
return link ? { ...link, current } : null
|
|
501
|
+
})
|
|
474
502
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
503
|
+
// Uniform list for rendering (#1353). Item pages: the single opposite-mode
|
|
504
|
+
// entry (modeToggle). Child pages (route carries meta.parent): the PARENT
|
|
505
|
+
// item's applicable pair — the page is in neither mode, so both View and
|
|
506
|
+
// Edit are offered, each under the same existence/permission rules. On a
|
|
507
|
+
// child page, `entityData` resolves to the parent item (last item segment
|
|
508
|
+
// of the chain), so the canUpdate gate is ownership-aware once hydrated.
|
|
509
|
+
const modeLinks = computed<BreadcrumbModeToggle[]>(() => {
|
|
510
|
+
const itemToggle = modeToggle.value
|
|
511
|
+
if (itemToggle) return [itemToggle]
|
|
512
|
+
|
|
513
|
+
void i18nLocale.value
|
|
514
|
+
if (!parentConfig.value || parentId.value == null) return []
|
|
515
|
+
const manager = getManager(parentConfig.value.entity)
|
|
516
|
+
if (!manager?.routePrefix) return []
|
|
517
|
+
|
|
518
|
+
return (['show', 'edit'] as const)
|
|
519
|
+
.map((target) => buildModeLink(manager, target, parentId.value, entityData.value))
|
|
520
|
+
.filter((link): link is BreadcrumbModeToggle => link !== null)
|
|
481
521
|
})
|
|
482
522
|
|
|
483
523
|
return {
|
|
@@ -494,6 +534,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
494
534
|
breadcrumb,
|
|
495
535
|
navlinks,
|
|
496
536
|
modeToggle,
|
|
537
|
+
modeLinks,
|
|
497
538
|
|
|
498
539
|
// Helpers
|
|
499
540
|
parentConfig,
|
|
@@ -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
|