qdadm 0.38.1 → 0.40.1
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
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ import { MockApiStorage, ApiStorage, SdkStorage } from 'qdadm'
|
|
|
26
26
|
// Auth
|
|
27
27
|
import { SessionAuthAdapter, LocalStorageSessionAuthAdapter } from 'qdadm'
|
|
28
28
|
|
|
29
|
+
// Security (permissions, roles)
|
|
30
|
+
import { SecurityChecker, PermissionMatcher, PermissionRegistry } from 'qdadm/security'
|
|
31
|
+
import { PersistableRoleGranterAdapter, createLocalStorageRoleGranter } from 'qdadm/security'
|
|
32
|
+
|
|
29
33
|
// Composables
|
|
30
34
|
import { useForm, useBareForm, useListPageBuilder } from 'qdadm/composables'
|
|
31
35
|
|
|
@@ -39,6 +43,33 @@ import { KernelContext } from 'qdadm/module'
|
|
|
39
43
|
import 'qdadm/styles'
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
## ctx.crud() - Route Helper
|
|
47
|
+
|
|
48
|
+
Register CRUD routes + navigation in one call:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
class BooksModule extends Module {
|
|
52
|
+
async connect(ctx) {
|
|
53
|
+
ctx.entity('books', { ... })
|
|
54
|
+
|
|
55
|
+
// Full CRUD with single form
|
|
56
|
+
ctx.crud('books', {
|
|
57
|
+
list: () => import('./pages/BookList.vue'),
|
|
58
|
+
form: () => import('./pages/BookForm.vue') // create + edit
|
|
59
|
+
}, {
|
|
60
|
+
nav: { section: 'Library', icon: 'pi pi-book' }
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// List only (read-only entity)
|
|
64
|
+
ctx.crud('settings', {
|
|
65
|
+
list: () => import('./pages/SettingsPage.vue')
|
|
66
|
+
}, { nav: { section: 'Config', icon: 'pi pi-cog' } })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Auto-generates: routes, route family, nav item. Use `ctx.routes()` for custom pages.
|
|
72
|
+
|
|
42
73
|
## SdkStorage
|
|
43
74
|
|
|
44
75
|
Adapter for generated SDK clients (hey-api, openapi-generator, etc.):
|
package/package.json
CHANGED
package/src/components/index.js
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* NotFoundPage - Default 404 page
|
|
4
|
+
*
|
|
5
|
+
* Simple page with link to home. Can be replaced via pages.notFound option.
|
|
6
|
+
*/
|
|
7
|
+
import { computed } from 'vue'
|
|
8
|
+
import { useRouter } from 'vue-router'
|
|
9
|
+
|
|
10
|
+
const router = useRouter()
|
|
11
|
+
|
|
12
|
+
const homeRoute = computed(() => {
|
|
13
|
+
// Find home route (first route or named 'home'/'dashboard')
|
|
14
|
+
const routes = router.getRoutes()
|
|
15
|
+
const home = routes.find(r => r.name === 'home' || r.name === 'dashboard')
|
|
16
|
+
return home?.name || '/'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function goHome() {
|
|
20
|
+
if (typeof homeRoute.value === 'string' && homeRoute.value.startsWith('/')) {
|
|
21
|
+
router.push(homeRoute.value)
|
|
22
|
+
} else {
|
|
23
|
+
router.push({ name: homeRoute.value })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="not-found-page">
|
|
30
|
+
<div class="not-found-content">
|
|
31
|
+
<div class="not-found-code">404</div>
|
|
32
|
+
<h1>Page not found</h1>
|
|
33
|
+
<p>The page you're looking for doesn't exist or has been moved.</p>
|
|
34
|
+
<button class="home-link" @click="goHome">
|
|
35
|
+
<i class="pi pi-home"></i>
|
|
36
|
+
Back to Home
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<style scoped>
|
|
43
|
+
.not-found-page {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
min-height: 100vh;
|
|
48
|
+
background: var(--p-surface-ground, #f8fafc);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.not-found-content {
|
|
52
|
+
text-align: center;
|
|
53
|
+
padding: 2rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.not-found-code {
|
|
57
|
+
font-size: 8rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
color: var(--p-primary-color, #3b82f6);
|
|
60
|
+
line-height: 1;
|
|
61
|
+
margin-bottom: 1rem;
|
|
62
|
+
opacity: 0.8;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h1 {
|
|
66
|
+
font-size: 1.5rem;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: var(--p-text-color, #1e293b);
|
|
69
|
+
margin: 0 0 0.5rem 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
p {
|
|
73
|
+
color: var(--p-text-muted-color, #64748b);
|
|
74
|
+
margin: 0 0 2rem 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.home-link {
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 0.5rem;
|
|
81
|
+
padding: 0.75rem 1.5rem;
|
|
82
|
+
background: var(--p-primary-color, #3b82f6);
|
|
83
|
+
color: white;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: 0.5rem;
|
|
86
|
+
font-size: 1rem;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: background 0.2s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.home-link:hover {
|
|
92
|
+
background: var(--p-primary-600, #2563eb);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
package/src/composables/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export { useBareForm } from './useBareForm'
|
|
6
6
|
export { useBreadcrumb } from './useBreadcrumb'
|
|
7
|
+
export { useSemanticBreadcrumb, computeSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
7
8
|
export { useDirtyState } from './useDirtyState'
|
|
8
9
|
export { useForm } from './useForm'
|
|
9
10
|
export { useFormPageBuilder } from './useFormPageBuilder'
|
|
@@ -1,99 +1,75 @@
|
|
|
1
1
|
import { computed, inject } from 'vue'
|
|
2
|
-
import {
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* useBreadcrumb -
|
|
6
|
+
* useBreadcrumb - Generate display-ready breadcrumb from semantic breadcrumb
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 2. Route path segments with smart label generation
|
|
8
|
+
* Uses useSemanticBreadcrumb for the semantic model, then applies adapters
|
|
9
|
+
* to resolve labels, paths, and icons for display.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* @param {object} options - Options
|
|
12
|
+
* @param {object} [options.entity] - Current entity data for dynamic labels
|
|
13
|
+
* @param {Function} [options.getEntityLabel] - Custom label resolver for entities
|
|
14
|
+
* @param {object} [options.labelMap] - Custom label mappings
|
|
15
|
+
* @param {object} [options.iconMap] - Custom icon mappings
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
* const { breadcrumbItems } = useBreadcrumb(
|
|
17
|
+
* @example
|
|
18
|
+
* const { breadcrumbItems, semanticBreadcrumb } = useBreadcrumb()
|
|
16
19
|
*
|
|
17
|
-
* //
|
|
18
|
-
* const { breadcrumbItems } = useBreadcrumb({
|
|
19
|
-
* entity: newsroomData,
|
|
20
|
-
* getEntityLabel: (entity) => entity.name || entity.slug
|
|
21
|
-
* })
|
|
22
|
-
*
|
|
23
|
-
* Route meta example:
|
|
24
|
-
* {
|
|
25
|
-
* path: 'agents/:id/edit',
|
|
26
|
-
* name: 'agent-edit',
|
|
27
|
-
* meta: {
|
|
28
|
-
* breadcrumb: [
|
|
29
|
-
* { label: 'Agents', to: { name: 'agents' } },
|
|
30
|
-
* { label: ':name', dynamic: true } // Resolved from entity.name
|
|
31
|
-
* ]
|
|
32
|
-
* }
|
|
33
|
-
* }
|
|
20
|
+
* // With entity data for dynamic labels
|
|
21
|
+
* const { breadcrumbItems } = useBreadcrumb({ entity: bookData })
|
|
34
22
|
*/
|
|
35
23
|
export function useBreadcrumb(options = {}) {
|
|
36
|
-
const route = useRoute()
|
|
37
24
|
const router = useRouter()
|
|
38
25
|
const homeRouteName = inject('qdadmHomeRoute', null)
|
|
39
26
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
27
|
+
// Get semantic breadcrumb
|
|
28
|
+
const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
|
|
29
|
+
|
|
30
|
+
// Default label mapping
|
|
31
|
+
const defaultLabelMap = {
|
|
42
32
|
home: 'Home',
|
|
43
33
|
dashboard: 'Dashboard',
|
|
44
|
-
users: 'Users',
|
|
45
|
-
roles: 'Roles',
|
|
46
|
-
apikeys: 'API Keys',
|
|
47
|
-
newsrooms: 'Newsrooms',
|
|
48
|
-
agents: 'Agents',
|
|
49
|
-
events: 'Events',
|
|
50
|
-
taxonomy: 'Taxonomy',
|
|
51
|
-
domains: 'Domains',
|
|
52
|
-
nexus: 'Nexus',
|
|
53
|
-
queue: 'Queue',
|
|
54
34
|
create: 'Create',
|
|
55
35
|
edit: 'Edit',
|
|
56
|
-
show: 'View'
|
|
36
|
+
show: 'View',
|
|
37
|
+
delete: 'Delete'
|
|
57
38
|
}
|
|
58
39
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
40
|
+
// Default icon mapping for entities
|
|
41
|
+
const defaultIconMap = {
|
|
61
42
|
users: 'pi pi-users',
|
|
62
43
|
roles: 'pi pi-shield',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
taxonomy: 'pi pi-tags',
|
|
68
|
-
domains: 'pi pi-globe',
|
|
69
|
-
nexus: 'pi pi-sitemap',
|
|
70
|
-
queue: 'pi pi-server'
|
|
44
|
+
books: 'pi pi-book',
|
|
45
|
+
genres: 'pi pi-tags',
|
|
46
|
+
loans: 'pi pi-exchange',
|
|
47
|
+
settings: 'pi pi-cog'
|
|
71
48
|
}
|
|
72
49
|
|
|
50
|
+
// Merge with custom mappings
|
|
51
|
+
const labelMap = { ...defaultLabelMap, ...options.labelMap }
|
|
52
|
+
const iconMap = { ...defaultIconMap, ...options.iconMap }
|
|
53
|
+
|
|
73
54
|
/**
|
|
74
55
|
* Capitalize first letter
|
|
75
56
|
*/
|
|
76
57
|
function capitalize(str) {
|
|
58
|
+
if (!str) return ''
|
|
77
59
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
78
60
|
}
|
|
79
61
|
|
|
80
62
|
/**
|
|
81
|
-
* Get
|
|
82
|
-
*/
|
|
83
|
-
function getLabel(segment) {
|
|
84
|
-
// Check labelMap first
|
|
85
|
-
if (labelMap[segment]) return labelMap[segment]
|
|
86
|
-
// Capitalize and replace hyphens
|
|
87
|
-
return capitalize(segment.replace(/-/g, ' '))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Resolve dynamic label from entity data
|
|
63
|
+
* Get route name for entity
|
|
92
64
|
*/
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
65
|
+
function getEntityRouteName(entity, kind) {
|
|
66
|
+
// Convention: entity-list -> 'book', entity-edit -> 'book-edit'
|
|
67
|
+
const singular = entity.endsWith('s') ? entity.slice(0, -1) : entity
|
|
68
|
+
if (kind === 'entity-list') return singular
|
|
69
|
+
if (kind === 'entity-show') return `${singular}-show`
|
|
70
|
+
if (kind === 'entity-edit') return `${singular}-edit`
|
|
71
|
+
if (kind === 'entity-create') return `${singular}-create`
|
|
72
|
+
return singular
|
|
97
73
|
}
|
|
98
74
|
|
|
99
75
|
/**
|
|
@@ -104,131 +80,114 @@ export function useBreadcrumb(options = {}) {
|
|
|
104
80
|
}
|
|
105
81
|
|
|
106
82
|
/**
|
|
107
|
-
*
|
|
83
|
+
* Resolve semantic item to display item
|
|
84
|
+
* @param {object} item - Semantic breadcrumb item
|
|
85
|
+
* @param {number} index - Index in the breadcrumb array
|
|
86
|
+
* @param {object} entity - Current entity data (for labels)
|
|
87
|
+
* @param {boolean} isLast - Whether this is the last item (no link)
|
|
108
88
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
89
|
+
function resolveItem(item, index, entity, isLast) {
|
|
90
|
+
const displayItem = {
|
|
91
|
+
label: '',
|
|
92
|
+
to: null,
|
|
93
|
+
icon: null
|
|
112
94
|
}
|
|
113
|
-
// Use label from labelMap or capitalize route name
|
|
114
|
-
const label = labelMap[homeRouteName] || capitalize(homeRouteName)
|
|
115
|
-
return { label, to: { name: homeRouteName }, icon: 'pi pi-home' }
|
|
116
|
-
}
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
if (item.kind === 'route') {
|
|
97
|
+
// Generic route
|
|
98
|
+
const routeName = item.route
|
|
99
|
+
displayItem.label = item.label || labelMap[routeName] || capitalize(routeName)
|
|
100
|
+
displayItem.to = isLast ? null : (routeExists(routeName) ? { name: routeName } : null)
|
|
101
|
+
if (routeName === 'home') {
|
|
102
|
+
displayItem.icon = 'pi pi-home'
|
|
103
|
+
}
|
|
104
|
+
} else if (item.kind.startsWith('entity-')) {
|
|
105
|
+
// Entity-related item
|
|
106
|
+
const entityName = item.entity
|
|
107
|
+
|
|
108
|
+
if (item.kind === 'entity-list') {
|
|
109
|
+
// Entity list - use plural label
|
|
110
|
+
displayItem.label = labelMap[entityName] || capitalize(entityName)
|
|
111
|
+
displayItem.icon = index === 1 ? iconMap[entityName] : null
|
|
112
|
+
const routeName = getEntityRouteName(entityName, item.kind)
|
|
113
|
+
displayItem.to = isLast ? null : (routeExists(routeName) ? { name: routeName } : null)
|
|
114
|
+
} else if (item.id) {
|
|
115
|
+
// Entity instance (show/edit/delete)
|
|
116
|
+
// Try to get label from entity data or use ID
|
|
117
|
+
if (entity && options.getEntityLabel) {
|
|
118
|
+
displayItem.label = options.getEntityLabel(entity)
|
|
119
|
+
} else if (entity?.name) {
|
|
120
|
+
displayItem.label = entity.name
|
|
121
|
+
} else if (entity?.title) {
|
|
122
|
+
displayItem.label = entity.title
|
|
123
|
+
} else {
|
|
124
|
+
displayItem.label = `#${item.id}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Last item = no link. Otherwise link to show page if exists
|
|
128
|
+
if (!isLast && (item.kind === 'entity-edit' || item.kind === 'entity-delete')) {
|
|
129
|
+
const showRouteName = getEntityRouteName(entityName, 'entity-show')
|
|
130
|
+
if (routeExists(showRouteName)) {
|
|
131
|
+
displayItem.to = { name: showRouteName, params: { id: item.id } }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (item.kind === 'entity-create') {
|
|
135
|
+
// Create page - label as action
|
|
136
|
+
displayItem.label = labelMap.create || 'Create'
|
|
131
137
|
}
|
|
132
|
-
items.push(resolved)
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
return
|
|
140
|
+
return displayItem
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
// Action segments that should be excluded from breadcrumb
|
|
139
|
-
const actionSegments = ['edit', 'create', 'show', 'view', 'new', 'delete']
|
|
140
|
-
|
|
141
143
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* Breadcrumb shows navigable parents only:
|
|
145
|
-
* - Excludes action segments (edit, create, show, etc.)
|
|
146
|
-
* - Excludes IDs
|
|
147
|
-
* - All items have links (to navigate back to parent)
|
|
144
|
+
* Get home breadcrumb item
|
|
148
145
|
*/
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const segments = route.path.split('/').filter(s => s && !s.startsWith(':'))
|
|
156
|
-
|
|
157
|
-
let currentPath = ''
|
|
158
|
-
for (let i = 0; i < segments.length; i++) {
|
|
159
|
-
const segment = segments[i]
|
|
160
|
-
currentPath += `/${segment}`
|
|
161
|
-
|
|
162
|
-
// Skip action segments (edit, create, show, etc.)
|
|
163
|
-
if (actionSegments.includes(segment.toLowerCase())) {
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Skip IDs: numeric, UUID, ULID, or any alphanumeric string > 10 chars
|
|
168
|
-
const isId = /^\d+$/.test(segment) || // numeric
|
|
169
|
-
segment.match(/^[0-9a-f-]{36}$/i) || // UUID
|
|
170
|
-
segment.match(/^[0-7][0-9a-hjkmnp-tv-z]{25}$/i) || // ULID
|
|
171
|
-
(segment.match(/^[a-z0-9]+$/i) && segment.length > 10) // Generated ID
|
|
172
|
-
if (isId) {
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Get label for this segment
|
|
177
|
-
const label = getLabel(segment)
|
|
178
|
-
|
|
179
|
-
// Find matching route for this path
|
|
180
|
-
const matchedRoute = router.getRoutes().find(r => {
|
|
181
|
-
const routePath = r.path.replace(/:\w+/g, '[^/]+')
|
|
182
|
-
const regex = new RegExp(`^${routePath}$`)
|
|
183
|
-
return regex.test(currentPath)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const item = {
|
|
187
|
-
label,
|
|
188
|
-
to: matchedRoute ? { name: matchedRoute.name } : null,
|
|
189
|
-
icon: i === 0 ? iconMap[segment] : null
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
items.push(item)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Remove last item if it matches current route (we only show parents)
|
|
196
|
-
if (items.length > 1) {
|
|
197
|
-
const lastItem = items[items.length - 1]
|
|
198
|
-
if (lastItem.to?.name === route.name) {
|
|
199
|
-
items.pop()
|
|
200
|
-
}
|
|
146
|
+
function getHomeItem() {
|
|
147
|
+
if (!homeRouteName || !routeExists(homeRouteName)) return null
|
|
148
|
+
return {
|
|
149
|
+
label: labelMap.home || 'Home',
|
|
150
|
+
to: { name: homeRouteName },
|
|
151
|
+
icon: 'pi pi-home'
|
|
201
152
|
}
|
|
202
|
-
|
|
203
|
-
return items
|
|
204
153
|
}
|
|
205
154
|
|
|
206
155
|
/**
|
|
207
|
-
* Computed breadcrumb items
|
|
156
|
+
* Computed display-ready breadcrumb items
|
|
208
157
|
*/
|
|
209
158
|
const breadcrumbItems = computed(() => {
|
|
210
159
|
const entity = options.entity?.value || options.entity
|
|
211
|
-
const
|
|
160
|
+
const items = []
|
|
161
|
+
|
|
162
|
+
// Add home first (display concern, not in semantic breadcrumb)
|
|
163
|
+
const homeItem = getHomeItem()
|
|
164
|
+
if (homeItem) {
|
|
165
|
+
items.push(homeItem)
|
|
166
|
+
}
|
|
212
167
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
168
|
+
// Resolve semantic items to display items
|
|
169
|
+
const semanticItems = semanticBreadcrumb.value
|
|
170
|
+
for (let i = 0; i < semanticItems.length; i++) {
|
|
171
|
+
const semanticItem = semanticItems[i]
|
|
172
|
+
const isLast = i === semanticItems.length - 1
|
|
173
|
+
const displayItem = resolveItem(semanticItem, i, entity, isLast)
|
|
174
|
+
items.push(displayItem)
|
|
216
175
|
}
|
|
217
176
|
|
|
218
|
-
|
|
219
|
-
return buildFromPath(entity, getEntityLabel)
|
|
177
|
+
return items
|
|
220
178
|
})
|
|
221
179
|
|
|
222
180
|
/**
|
|
223
181
|
* Manual override - set custom breadcrumb items
|
|
224
182
|
*/
|
|
225
183
|
function setBreadcrumb(items) {
|
|
226
|
-
const
|
|
227
|
-
return
|
|
184
|
+
const homeItem = { label: labelMap.home || 'Home', to: { name: homeRouteName }, icon: 'pi pi-home' }
|
|
185
|
+
return homeRouteName && routeExists(homeRouteName) ? [homeItem, ...items] : items
|
|
228
186
|
}
|
|
229
187
|
|
|
230
188
|
return {
|
|
231
189
|
breadcrumbItems,
|
|
190
|
+
semanticBreadcrumb,
|
|
232
191
|
setBreadcrumb
|
|
233
192
|
}
|
|
234
193
|
}
|
|
@@ -1547,6 +1547,7 @@ export function useListPageBuilder(config = {}) {
|
|
|
1547
1547
|
selectable: hasBulkActions.value,
|
|
1548
1548
|
|
|
1549
1549
|
// Pagination
|
|
1550
|
+
lazy: true, // Let parent handle pagination via @page events
|
|
1550
1551
|
totalRecords: totalRecords.value,
|
|
1551
1552
|
rows: pageSize.value,
|
|
1552
1553
|
rowsPerPageOptions,
|