qdadm 0.38.1 → 0.40.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 +31 -0
- package/package.json +1 -1
- package/src/components/index.js +1 -0
- package/src/components/pages/NotFoundPage.vue +94 -0
- package/src/composables/index.js +1 -0
- package/src/composables/useBreadcrumb.js +119 -160
- package/src/composables/useListPageBuilder.js +1 -0
- package/src/composables/useNavContext.js +69 -154
- package/src/composables/useNavigation.js +22 -6
- package/src/composables/useSemanticBreadcrumb.js +182 -0
- package/src/debug/DebugModule.js +6 -0
- package/src/debug/RouterCollector.js +235 -0
- package/src/debug/components/DebugBar.vue +22 -9
- package/src/debug/components/panels/RouterPanel.vue +657 -0
- package/src/debug/components/panels/index.js +1 -0
- package/src/debug/index.js +1 -0
- package/src/kernel/Kernel.js +12 -0
- package/src/kernel/KernelContext.js +15 -6
- package/src/orchestrator/Orchestrator.js +11 -1
|
@@ -1,43 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useNavContext - Route-aware navigation context for breadcrumb and navlinks
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Uses semantic breadcrumb as the source of truth for navigation structure.
|
|
5
|
+
* Semantic breadcrumb is computed from route path and registered routes.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - meta.entity: Entity managed by this route (required)
|
|
13
|
-
* - meta.parent: Parent entity config for nested routes
|
|
14
|
-
* - parent.entity: Parent entity name
|
|
15
|
-
* - parent.param: Route param for parent ID
|
|
7
|
+
* Semantic breadcrumb kinds:
|
|
8
|
+
* - entity-list: Entity collection (e.g., /books)
|
|
9
|
+
* - entity-show: Entity instance view (e.g., /books/1)
|
|
10
|
+
* - entity-edit: Entity instance edit (e.g., /books/1/edit)
|
|
11
|
+
* - entity-create: Entity creation (e.g., /books/create)
|
|
12
|
+
* - route: Generic route (e.g., /settings)
|
|
16
13
|
*
|
|
17
14
|
* Examples:
|
|
18
|
-
* Path: /books
|
|
19
|
-
* →
|
|
20
|
-
*
|
|
21
|
-
* Path: /books/:id/edit meta: { entity: 'books' }
|
|
22
|
-
* → Home > Books > "Le Petit Prince"
|
|
23
|
-
*
|
|
24
|
-
* Path: /books/:bookId/loans meta: { entity: 'loans', parent: { entity: 'books', param: 'bookId' } }
|
|
25
|
-
* → Home > Books > "Le Petit Prince" > Loans
|
|
26
|
-
*
|
|
27
|
-
* Path: /books/:bookId/loans/:id/edit meta: { entity: 'loans', parent: { entity: 'books', param: 'bookId' } }
|
|
28
|
-
* → Home > Books > "Le Petit Prince" > Loans > "Loan #abc123"
|
|
15
|
+
* Path: /books → [{ kind: 'entity-list', entity: 'books' }]
|
|
16
|
+
* Path: /books/1/edit → [{ kind: 'entity-list', entity: 'books' }, { kind: 'entity-edit', entity: 'books', id: '1' }]
|
|
17
|
+
* Path: /books/stats → [{ kind: 'entity-list', entity: 'books' }, { kind: 'route', route: 'book-stats' }]
|
|
29
18
|
*/
|
|
30
19
|
import { ref, computed, watch, inject } from 'vue'
|
|
31
20
|
import { useRoute, useRouter } from 'vue-router'
|
|
32
21
|
import { getSiblingRoutes } from '../module/moduleRegistry.js'
|
|
33
|
-
|
|
34
|
-
// Action segments that don't appear in breadcrumb
|
|
35
|
-
const ACTION_SEGMENTS = ['edit', 'create', 'new', 'show', 'view', 'delete']
|
|
22
|
+
import { useSemanticBreadcrumb } from './useSemanticBreadcrumb.js'
|
|
36
23
|
|
|
37
24
|
export function useNavContext(options = {}) {
|
|
38
25
|
const route = useRoute()
|
|
39
26
|
const router = useRouter()
|
|
40
27
|
|
|
28
|
+
// Semantic breadcrumb as source of truth
|
|
29
|
+
const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
|
|
30
|
+
|
|
41
31
|
// Injected dependencies
|
|
42
32
|
const orchestrator = inject('qdadmOrchestrator', null)
|
|
43
33
|
const homeRouteName = inject('qdadmHomeRoute', null)
|
|
@@ -48,9 +38,6 @@ export function useNavContext(options = {}) {
|
|
|
48
38
|
// or injected from parent (for child pages)
|
|
49
39
|
const breadcrumbEntities = options.breadcrumbEntities ?? inject('qdadmBreadcrumbEntities', null)
|
|
50
40
|
|
|
51
|
-
// Entity data cache
|
|
52
|
-
const entityDataCache = ref(new Map())
|
|
53
|
-
|
|
54
41
|
function getManager(entityName) {
|
|
55
42
|
return orchestrator?.get(entityName)
|
|
56
43
|
}
|
|
@@ -59,122 +46,53 @@ export function useNavContext(options = {}) {
|
|
|
59
46
|
return router.getRoutes().some(r => r.name === name)
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
// ============================================================================
|
|
63
|
-
// PATH PATTERN ANALYSIS
|
|
64
|
-
// ============================================================================
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse route path pattern into typed segments
|
|
68
|
-
*
|
|
69
|
-
* Input: '/books/:bookId/loans/:id/edit'
|
|
70
|
-
* Output: [
|
|
71
|
-
* { type: 'static', value: 'books' },
|
|
72
|
-
* { type: 'param', value: 'bookId' },
|
|
73
|
-
* { type: 'static', value: 'loans' },
|
|
74
|
-
* { type: 'param', value: 'id' },
|
|
75
|
-
* { type: 'action', value: 'edit' }
|
|
76
|
-
* ]
|
|
77
|
-
*/
|
|
78
|
-
function parsePathPattern(pathPattern) {
|
|
79
|
-
const segments = []
|
|
80
|
-
const parts = pathPattern.split('/').filter(Boolean)
|
|
81
|
-
|
|
82
|
-
for (const part of parts) {
|
|
83
|
-
if (part.startsWith(':')) {
|
|
84
|
-
// Param segment: :id, :bookId
|
|
85
|
-
segments.push({ type: 'param', value: part.slice(1) })
|
|
86
|
-
} else if (ACTION_SEGMENTS.includes(part.toLowerCase())) {
|
|
87
|
-
// Action segment: edit, create, show
|
|
88
|
-
segments.push({ type: 'action', value: part })
|
|
89
|
-
} else {
|
|
90
|
-
// Static segment: books, loans
|
|
91
|
-
segments.push({ type: 'static', value: part })
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return segments
|
|
96
|
-
}
|
|
97
|
-
|
|
98
49
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* Uses route meta to know which entity each static segment represents.
|
|
102
|
-
* The meta.parent chain declares the entity hierarchy.
|
|
50
|
+
* Convert semantic breadcrumb to navigation chain format
|
|
51
|
+
* Maps semantic kinds to chain types for compatibility
|
|
103
52
|
*/
|
|
104
|
-
function
|
|
53
|
+
function semanticToNavChain(semantic) {
|
|
105
54
|
const chain = []
|
|
106
|
-
const meta = routeMeta || {}
|
|
107
|
-
|
|
108
|
-
// Collect all entities in the hierarchy (parent chain + current)
|
|
109
|
-
const entityHierarchy = []
|
|
110
|
-
|
|
111
|
-
// Build parent chain (oldest ancestor first)
|
|
112
|
-
function collectParents(parentConfig) {
|
|
113
|
-
if (!parentConfig) return
|
|
114
|
-
const parentManager = getManager(parentConfig.entity)
|
|
115
|
-
if (!parentManager) return
|
|
116
|
-
|
|
117
|
-
// Check if this parent has its own parent
|
|
118
|
-
const parentRoute = router.getRoutes().find(r =>
|
|
119
|
-
r.name === `${parentManager.routePrefix}-edit`
|
|
120
|
-
)
|
|
121
|
-
if (parentRoute?.meta?.parent) {
|
|
122
|
-
collectParents(parentRoute.meta.parent)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
entityHierarchy.push({
|
|
126
|
-
entity: parentConfig.entity,
|
|
127
|
-
manager: parentManager,
|
|
128
|
-
idParam: parentConfig.param
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
collectParents(meta.parent)
|
|
133
55
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
entity:
|
|
140
|
-
manager
|
|
141
|
-
|
|
56
|
+
for (const item of semantic) {
|
|
57
|
+
if (item.kind === 'entity-list') {
|
|
58
|
+
const manager = getManager(item.entity)
|
|
59
|
+
chain.push({
|
|
60
|
+
type: 'list',
|
|
61
|
+
entity: item.entity,
|
|
62
|
+
manager,
|
|
63
|
+
label: manager?.labelPlural || item.entity,
|
|
64
|
+
routeName: manager?.routePrefix || item.entity.slice(0, -1)
|
|
142
65
|
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Now build chain from hierarchy
|
|
147
|
-
for (const { entity, manager, idParam } of entityHierarchy) {
|
|
148
|
-
const entityId = routeParams[idParam]
|
|
149
|
-
|
|
150
|
-
// Add list segment
|
|
151
|
-
chain.push({
|
|
152
|
-
type: 'list',
|
|
153
|
-
entity,
|
|
154
|
-
manager,
|
|
155
|
-
label: manager.labelPlural || manager.name,
|
|
156
|
-
routeName: manager.routePrefix
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
// Add item segment if we have an ID for this entity
|
|
160
|
-
if (entityId) {
|
|
66
|
+
} else if (item.kind.startsWith('entity-') && item.id) {
|
|
67
|
+
// entity-show, entity-edit, entity-delete
|
|
68
|
+
const manager = getManager(item.entity)
|
|
161
69
|
chain.push({
|
|
162
70
|
type: 'item',
|
|
163
|
-
entity,
|
|
71
|
+
entity: item.entity,
|
|
164
72
|
manager,
|
|
165
|
-
id:
|
|
166
|
-
routeName: `${manager.routePrefix}-edit`
|
|
73
|
+
id: item.id,
|
|
74
|
+
routeName: manager ? `${manager.routePrefix}-edit` : null
|
|
75
|
+
})
|
|
76
|
+
} else if (item.kind === 'entity-create') {
|
|
77
|
+
// Create page - treat as special list
|
|
78
|
+
const manager = getManager(item.entity)
|
|
79
|
+
chain.push({
|
|
80
|
+
type: 'create',
|
|
81
|
+
entity: item.entity,
|
|
82
|
+
manager,
|
|
83
|
+
label: 'Create',
|
|
84
|
+
routeName: manager ? `${manager.routePrefix}-create` : null
|
|
85
|
+
})
|
|
86
|
+
} else if (item.kind === 'route') {
|
|
87
|
+
// Generic route (like /books/stats)
|
|
88
|
+
// Use label from semantic item if provided (custom breadcrumb), else lookup route
|
|
89
|
+
const routeRecord = router.getRoutes().find(r => r.name === item.route)
|
|
90
|
+
chain.push({
|
|
91
|
+
type: 'route',
|
|
92
|
+
route: item.route,
|
|
93
|
+
label: item.label || routeRecord?.meta?.navLabel || routeRecord?.meta?.title || item.route,
|
|
94
|
+
routeName: item.route
|
|
167
95
|
})
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Handle child-list case: when on a child route without :id
|
|
172
|
-
// The last segment is a child-list, not a regular list
|
|
173
|
-
if (meta.parent && !routeParams.id && chain.length > 0) {
|
|
174
|
-
const lastSegment = chain[chain.length - 1]
|
|
175
|
-
if (lastSegment.type === 'list') {
|
|
176
|
-
lastSegment.type = 'child-list'
|
|
177
|
-
lastSegment.navLabel = meta.navLabel
|
|
178
96
|
}
|
|
179
97
|
}
|
|
180
98
|
|
|
@@ -186,23 +104,10 @@ export function useNavContext(options = {}) {
|
|
|
186
104
|
// ============================================================================
|
|
187
105
|
|
|
188
106
|
/**
|
|
189
|
-
*
|
|
190
|
-
*/
|
|
191
|
-
const pathSegments = computed(() => {
|
|
192
|
-
// Get the matched route's path pattern
|
|
193
|
-
const matched = route.matched
|
|
194
|
-
if (!matched.length) return []
|
|
195
|
-
|
|
196
|
-
// Use the last matched route's full path
|
|
197
|
-
const lastMatch = matched[matched.length - 1]
|
|
198
|
-
return parsePathPattern(lastMatch.path)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Navigation chain built from path analysis
|
|
107
|
+
* Navigation chain built from semantic breadcrumb
|
|
203
108
|
*/
|
|
204
109
|
const navChain = computed(() => {
|
|
205
|
-
return
|
|
110
|
+
return semanticToNavChain(semanticBreadcrumb.value)
|
|
206
111
|
})
|
|
207
112
|
|
|
208
113
|
// ============================================================================
|
|
@@ -293,15 +198,25 @@ export function useNavContext(options = {}) {
|
|
|
293
198
|
if (segment.type === 'list') {
|
|
294
199
|
items.push({
|
|
295
200
|
label: segment.label,
|
|
296
|
-
to: { name: segment.routeName }
|
|
201
|
+
to: isLast ? null : (segment.routeName && routeExists(segment.routeName) ? { name: segment.routeName } : null)
|
|
297
202
|
})
|
|
298
203
|
} else if (segment.type === 'item') {
|
|
299
204
|
const data = chainData.value.get(i)
|
|
300
|
-
const label = data ? segment.manager.getEntityLabel(data) : '...'
|
|
205
|
+
const label = data && segment.manager ? segment.manager.getEntityLabel(data) : '...'
|
|
301
206
|
|
|
302
207
|
items.push({
|
|
303
208
|
label,
|
|
304
|
-
to: isLast ? null : { name: segment.routeName, params: { id: segment.id } }
|
|
209
|
+
to: isLast ? null : (segment.routeName && routeExists(segment.routeName) ? { name: segment.routeName, params: { id: segment.id } } : null)
|
|
210
|
+
})
|
|
211
|
+
} else if (segment.type === 'create') {
|
|
212
|
+
items.push({
|
|
213
|
+
label: segment.label
|
|
214
|
+
})
|
|
215
|
+
} else if (segment.type === 'route') {
|
|
216
|
+
// Generic route (e.g., /books/stats)
|
|
217
|
+
items.push({
|
|
218
|
+
label: segment.label,
|
|
219
|
+
to: isLast ? null : (segment.routeName && routeExists(segment.routeName) ? { name: segment.routeName } : null)
|
|
305
220
|
})
|
|
306
221
|
} else if (segment.type === 'child-list') {
|
|
307
222
|
items.push({
|
|
@@ -383,8 +298,8 @@ export function useNavContext(options = {}) {
|
|
|
383
298
|
|
|
384
299
|
return {
|
|
385
300
|
// Analysis
|
|
386
|
-
pathSegments,
|
|
387
301
|
navChain,
|
|
302
|
+
semanticBreadcrumb,
|
|
388
303
|
|
|
389
304
|
// Data
|
|
390
305
|
entityData,
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { computed, inject, ref, onMounted } from 'vue'
|
|
13
13
|
import { useRoute, useRouter } from 'vue-router'
|
|
14
|
-
import { getNavSections,
|
|
14
|
+
import { getNavSections, alterMenuSections, isMenuAltered } from '../module/moduleRegistry'
|
|
15
|
+
import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Navigation composable
|
|
@@ -31,6 +32,9 @@ export function useNavigation() {
|
|
|
31
32
|
const orchestrator = inject('qdadmOrchestrator', null)
|
|
32
33
|
const hooks = inject('qdadmHooks', null)
|
|
33
34
|
|
|
35
|
+
// Semantic breadcrumb for entity-based active detection
|
|
36
|
+
const { breadcrumb } = useSemanticBreadcrumb()
|
|
37
|
+
|
|
34
38
|
// Track whether menu:alter has completed
|
|
35
39
|
const isReady = ref(isMenuAltered())
|
|
36
40
|
|
|
@@ -74,17 +78,29 @@ export function useNavigation() {
|
|
|
74
78
|
|
|
75
79
|
/**
|
|
76
80
|
* Check if a nav item is currently active
|
|
81
|
+
* Uses semantic breadcrumb only - no route segment deduction
|
|
77
82
|
*/
|
|
78
83
|
function isNavActive(item) {
|
|
79
|
-
const currentRouteName = route.name
|
|
80
|
-
|
|
81
84
|
// Exact match mode
|
|
82
85
|
if (item.exact) {
|
|
83
|
-
return
|
|
86
|
+
return route.name === item.route
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check semantic breadcrumb for match
|
|
90
|
+
const bc = breadcrumb.value
|
|
91
|
+
|
|
92
|
+
// Entity-based menu item: match ONLY by entity (don't fall through)
|
|
93
|
+
if (item.entity) {
|
|
94
|
+
const firstEntityItem = bc.find(b => b.kind.startsWith('entity-'))
|
|
95
|
+
return firstEntityItem ? firstEntityItem.entity === item.entity : false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Route-based menu item: check if route is in breadcrumb
|
|
99
|
+
if (item.route) {
|
|
100
|
+
return bc.some(b => b.kind === 'route' && b.route === item.route)
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
return isRouteInFamily(currentRouteName, item.route)
|
|
103
|
+
return false
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
/**
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Action segments mapping to entity kinds
|
|
6
|
+
*/
|
|
7
|
+
const ACTION_MAP = {
|
|
8
|
+
edit: 'entity-edit',
|
|
9
|
+
create: 'entity-create',
|
|
10
|
+
new: 'entity-create',
|
|
11
|
+
show: 'entity-show',
|
|
12
|
+
view: 'entity-show',
|
|
13
|
+
delete: 'entity-delete'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compute semantic breadcrumb from route path and routes list
|
|
18
|
+
*
|
|
19
|
+
* Pure function that can be used both in Vue composables and outside components.
|
|
20
|
+
*
|
|
21
|
+
* If the matched route has `meta.breadcrumb`, that array is used directly.
|
|
22
|
+
* This allows routes to define a custom breadcrumb structure.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} path - Current route path (e.g., '/books/1/edit')
|
|
25
|
+
* @param {Array} routes - List of all registered routes from router.getRoutes()
|
|
26
|
+
* @param {object} [currentRoute] - Current route object (optional, for meta.breadcrumb support)
|
|
27
|
+
* @returns {Array} Semantic breadcrumb items
|
|
28
|
+
*/
|
|
29
|
+
export function computeSemanticBreadcrumb(path, routes, currentRoute = null) {
|
|
30
|
+
if (!path) return []
|
|
31
|
+
|
|
32
|
+
// Check if route defines a custom breadcrumb
|
|
33
|
+
if (currentRoute?.meta?.breadcrumb) {
|
|
34
|
+
return currentRoute.meta.breadcrumb
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const items = []
|
|
38
|
+
|
|
39
|
+
// Filter out catch-all and not-found routes
|
|
40
|
+
const validRoutes = routes.filter(r =>
|
|
41
|
+
r.name !== 'not-found' &&
|
|
42
|
+
!r.path.includes('*') &&
|
|
43
|
+
!r.path.includes(':pathMatch')
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Note: Home is NOT included in semantic breadcrumb
|
|
47
|
+
// It's a display concern - add it in useBreadcrumb if needed
|
|
48
|
+
|
|
49
|
+
const segments = path.split('/').filter(Boolean)
|
|
50
|
+
let currentPath = ''
|
|
51
|
+
let lastEntity = null
|
|
52
|
+
let pendingId = null // Track unmatched segments that could be IDs
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < segments.length; i++) {
|
|
55
|
+
const segment = segments[i]
|
|
56
|
+
const nextSegment = segments[i + 1]
|
|
57
|
+
currentPath += `/${segment}`
|
|
58
|
+
|
|
59
|
+
// Check if this is an action segment
|
|
60
|
+
const actionKind = ACTION_MAP[segment.toLowerCase()]
|
|
61
|
+
if (actionKind && lastEntity) {
|
|
62
|
+
// If we have a pending ID, add a new entity item with action kind
|
|
63
|
+
if (pendingId) {
|
|
64
|
+
items.push({ kind: actionKind, entity: lastEntity, id: pendingId })
|
|
65
|
+
pendingId = null
|
|
66
|
+
} else {
|
|
67
|
+
// No pending ID - update last item's kind (e.g., /books/create)
|
|
68
|
+
const lastItem = items[items.length - 1]
|
|
69
|
+
if (lastItem && lastItem.kind.startsWith('entity-') && !lastItem.id) {
|
|
70
|
+
lastItem.kind = actionKind
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find matching route
|
|
77
|
+
const matchedRoute = validRoutes.find(r => {
|
|
78
|
+
if (r.path === currentPath) return true
|
|
79
|
+
const routeSegs = r.path.split('/').filter(Boolean)
|
|
80
|
+
const pathSegs = currentPath.split('/').filter(Boolean)
|
|
81
|
+
if (routeSegs.length !== pathSegs.length) return false
|
|
82
|
+
return routeSegs.every((rs, idx) => rs.startsWith(':') || rs === pathSegs[idx])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!matchedRoute) {
|
|
86
|
+
// No route found - this segment could be an ID (e.g., /books/1 when route is /books/:id/edit)
|
|
87
|
+
if (lastEntity) {
|
|
88
|
+
pendingId = segment
|
|
89
|
+
}
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entity = matchedRoute.meta?.entity || null
|
|
94
|
+
const isParam = matchedRoute.path.split('/').filter(Boolean).some((s, idx) => {
|
|
95
|
+
const pathSegs = currentPath.split('/').filter(Boolean)
|
|
96
|
+
return s.startsWith(':') && idx === pathSegs.length - 1
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (entity) {
|
|
100
|
+
lastEntity = entity
|
|
101
|
+
pendingId = null // Clear pending ID when we match a route
|
|
102
|
+
if (isParam) {
|
|
103
|
+
// Entity instance - get param value (the ID)
|
|
104
|
+
const paramValue = segment
|
|
105
|
+
// Determine kind based on next segment or route name
|
|
106
|
+
let kind = 'entity-show'
|
|
107
|
+
if (nextSegment && ACTION_MAP[nextSegment.toLowerCase()]) {
|
|
108
|
+
kind = ACTION_MAP[nextSegment.toLowerCase()]
|
|
109
|
+
} else if (matchedRoute.name?.includes('edit')) {
|
|
110
|
+
kind = 'entity-edit'
|
|
111
|
+
}
|
|
112
|
+
items.push({ kind, entity, id: paramValue })
|
|
113
|
+
} else {
|
|
114
|
+
// Entity list
|
|
115
|
+
items.push({ kind: 'entity-list', entity })
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Generic route
|
|
119
|
+
items.push({ kind: 'route', route: matchedRoute.name || segment })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return items
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* useSemanticBreadcrumb - Vue composable for semantic breadcrumb
|
|
128
|
+
*
|
|
129
|
+
* Returns semantic objects per level - adapters resolve labels/paths for display.
|
|
130
|
+
*
|
|
131
|
+
* Kinds:
|
|
132
|
+
* - route: Generic route (e.g., home, dashboard)
|
|
133
|
+
* - entity-list: Entity collection (e.g., /books)
|
|
134
|
+
* - entity-show: Entity instance view (e.g., /books/1)
|
|
135
|
+
* - entity-edit: Entity instance edit (e.g., /books/1/edit)
|
|
136
|
+
* - entity-create: Entity creation (e.g., /books/create)
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const { breadcrumb } = useSemanticBreadcrumb()
|
|
140
|
+
* // For /books/1/edit returns:
|
|
141
|
+
* // [
|
|
142
|
+
* // { kind: 'route', route: 'home' },
|
|
143
|
+
* // { kind: 'entity-list', entity: 'books' },
|
|
144
|
+
* // { kind: 'entity-edit', entity: 'books', id: '1' }
|
|
145
|
+
* // ]
|
|
146
|
+
*/
|
|
147
|
+
export function useSemanticBreadcrumb() {
|
|
148
|
+
const route = useRoute()
|
|
149
|
+
const router = useRouter()
|
|
150
|
+
|
|
151
|
+
const breadcrumb = computed(() => {
|
|
152
|
+
return computeSemanticBreadcrumb(route.path, router.getRoutes(), route)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if an entity is active (present in current breadcrumb)
|
|
157
|
+
* Useful for menu highlighting
|
|
158
|
+
*
|
|
159
|
+
* @param {string} entity - Entity name to check
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
function isEntityActive(entity) {
|
|
163
|
+
return breadcrumb.value.some(item =>
|
|
164
|
+
item.kind.startsWith('entity-') && item.entity === entity
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the active entity from breadcrumb (first entity found)
|
|
170
|
+
* @returns {string|null}
|
|
171
|
+
*/
|
|
172
|
+
const activeEntity = computed(() => {
|
|
173
|
+
const entityItem = breadcrumb.value.find(item => item.kind.startsWith('entity-'))
|
|
174
|
+
return entityItem?.entity || null
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
breadcrumb,
|
|
179
|
+
isEntityActive,
|
|
180
|
+
activeEntity
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/debug/DebugModule.js
CHANGED
|
@@ -27,6 +27,7 @@ import { ToastCollector } from './ToastCollector.js'
|
|
|
27
27
|
import { ZonesCollector } from './ZonesCollector.js'
|
|
28
28
|
import { AuthCollector } from './AuthCollector.js'
|
|
29
29
|
import { EntitiesCollector } from './EntitiesCollector.js'
|
|
30
|
+
import { RouterCollector } from './RouterCollector.js'
|
|
30
31
|
import DebugBar from './components/DebugBar.vue'
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -92,6 +93,7 @@ export class DebugModule extends Module {
|
|
|
92
93
|
* @param {boolean} [options.zonesCollector=true] - Include ZonesCollector
|
|
93
94
|
* @param {boolean} [options.authCollector=true] - Include AuthCollector
|
|
94
95
|
* @param {boolean} [options.entitiesCollector=true] - Include EntitiesCollector
|
|
96
|
+
* @param {boolean} [options.routerCollector=true] - Include RouterCollector
|
|
95
97
|
*/
|
|
96
98
|
constructor(options = {}) {
|
|
97
99
|
super(options)
|
|
@@ -155,6 +157,10 @@ export class DebugModule extends Module {
|
|
|
155
157
|
this._bridge.addCollector(new EntitiesCollector(collectorOptions))
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
if (this.options.routerCollector !== false) {
|
|
161
|
+
this._bridge.addCollector(new RouterCollector(collectorOptions))
|
|
162
|
+
}
|
|
163
|
+
|
|
158
164
|
// Install collectors with context
|
|
159
165
|
this._bridge.install(ctx)
|
|
160
166
|
|