qdadm 0.48.0 → 0.49.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.48.0",
3
+ "version": "0.49.1",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -177,9 +177,12 @@ function setBreadcrumbEntity(data, level = 1) {
177
177
  provide('qdadmSetBreadcrumbEntity', setBreadcrumbEntity)
178
178
  provide('qdadmBreadcrumbEntities', breadcrumbEntities)
179
179
 
180
- // Clear entity data on route change (before new page mounts)
180
+ // Clear entity data and overrides on route change (before new page mounts)
181
+ // This ensures list pages get default breadcrumb, detail pages can override via PageNav
181
182
  watch(() => route.fullPath, () => {
182
183
  breadcrumbEntities.value = new Map()
184
+ breadcrumbOverride.value = null
185
+ navlinksOverride.value = null
183
186
  })
184
187
 
185
188
  // Navigation context (breadcrumb + navlinks from route config)
@@ -17,15 +17,17 @@
17
17
  * - entity: Current entity data (for dynamic labels in breadcrumb)
18
18
  * - parentEntity: Parent entity data (for parent label in breadcrumb)
19
19
  */
20
- import { computed, ref, watch, onMounted, onUnmounted, inject } from 'vue'
20
+ import { computed, ref, watch, inject } from 'vue'
21
21
  import { useRoute, useRouter } from 'vue-router'
22
- import { getSiblingRoutes } from '../../module/moduleRegistry.js'
22
+ import { getSiblingRoutes, getChildRoutes } from '../../module/moduleRegistry.js'
23
23
  import { useOrchestrator } from '../../orchestrator/useOrchestrator.js'
24
24
 
25
25
  // Inject override refs from AppLayout
26
26
  const breadcrumbOverride = inject('qdadmBreadcrumbOverride', null)
27
27
  const navlinksOverride = inject('qdadmNavlinksOverride', null)
28
28
  const homeRouteName = inject('qdadmHomeRoute', null)
29
+ // Entity data set by useEntityItemPage via setBreadcrumbEntity
30
+ const breadcrumbEntities = inject('qdadmBreadcrumbEntities', null)
29
31
 
30
32
  const props = defineProps({
31
33
  entity: { type: Object, default: null },
@@ -85,10 +87,22 @@ const breadcrumbItems = computed(() => {
85
87
  if (entityName) {
86
88
  const manager = getManager(entityName)
87
89
  if (manager) {
90
+ // Entity list link
88
91
  items.push({
89
92
  label: manager.labelPlural || manager.name,
90
93
  to: { name: manager.routePrefix }
91
94
  })
95
+
96
+ // If on detail page (has :id param), add current entity item
97
+ const entityId = route.params.id
98
+ if (entityId) {
99
+ // Get entity data from props or from breadcrumbEntities (set by useEntityItemPage)
100
+ const entityData = props.entity || breadcrumbEntities?.value?.get(1)
101
+ const entityLabel = entityData
102
+ ? manager.getEntityLabel(entityData)
103
+ : '...'
104
+ items.push({ label: entityLabel })
105
+ }
92
106
  }
93
107
  }
94
108
  return items
@@ -129,7 +143,7 @@ const breadcrumbItems = computed(() => {
129
143
  })
130
144
 
131
145
  // Sibling navlinks (routes with same parent)
132
- const navlinks = computed(() => {
146
+ const siblingNavlinks = computed(() => {
133
147
  if (!parentConfig.value) return []
134
148
 
135
149
  const { entity: parentEntityName, param } = parentConfig.value
@@ -148,52 +162,91 @@ const navlinks = computed(() => {
148
162
  })
149
163
  })
150
164
 
151
- // Also include parent "Details" link
165
+ // Child navlinks (when on parent detail page, show links to children)
166
+ const childNavlinks = computed(() => {
167
+ // Only when NOT on a child route (no parentConfig)
168
+ if (parentConfig.value) return []
169
+
170
+ const entityName = route.meta?.entity
171
+ if (!entityName) return []
172
+
173
+ const children = getChildRoutes(entityName)
174
+ if (children.length === 0) return []
175
+
176
+ const entityId = route.params.id
177
+
178
+ // Build navlinks to child routes
179
+ return children.map(childRoute => {
180
+ const childManager = childRoute.meta?.entity ? getManager(childRoute.meta.entity) : null
181
+ const label = childRoute.meta?.navLabel || childManager?.labelPlural || childRoute.name
182
+ const parentParam = childRoute.meta?.parent?.param || 'id'
183
+
184
+ return {
185
+ label,
186
+ to: { name: childRoute.name, params: { [parentParam]: entityId } },
187
+ active: false
188
+ }
189
+ })
190
+ })
191
+
192
+ // Combined navlinks with "Details" link
152
193
  const allNavlinks = computed(() => {
153
- if (!parentConfig.value) return []
194
+ // Case 1: On a child route - show siblings + Details link to parent
195
+ if (parentConfig.value) {
196
+ const { entity: parentEntityName, param, itemRoute } = parentConfig.value
197
+ const parentId = route.params[param]
198
+ const parentManager = getManager(parentEntityName)
154
199
 
155
- const { entity: parentEntityName, param, itemRoute } = parentConfig.value
156
- const parentId = route.params[param]
157
- const parentManager = getManager(parentEntityName)
200
+ if (!parentManager) return siblingNavlinks.value
158
201
 
159
- if (!parentManager) return navlinks.value
202
+ const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
203
+ const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
204
+ const isOnParentRoute = route.name === parentRouteName
205
+
206
+ // Details link to parent item page
207
+ // CONVENTION: Entity item routes MUST use :id as param name (e.g., /books/:id)
208
+ // This is a qdadm convention - all entity detail/edit routes expect 'id' param
209
+ const detailsLink = {
210
+ label: 'Details',
211
+ to: { name: parentRouteName, params: { id: parentId } },
212
+ active: isOnParentRoute
213
+ }
214
+
215
+ return [detailsLink, ...siblingNavlinks.value]
216
+ }
160
217
 
161
- const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
162
- const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
163
- const isOnParentRoute = route.name === parentRouteName
218
+ // Case 2: On parent detail page - show children + Details (active)
219
+ if (childNavlinks.value.length > 0) {
220
+ const detailsLink = {
221
+ label: 'Details',
222
+ to: { name: route.name, params: route.params },
223
+ active: true
224
+ }
164
225
 
165
- // Details link to parent item page
166
- const detailsLink = {
167
- label: 'Details',
168
- to: { name: parentRouteName, params: { id: parentId } },
169
- active: isOnParentRoute
226
+ return [detailsLink, ...childNavlinks.value]
170
227
  }
171
228
 
172
- return [detailsLink, ...navlinks.value]
229
+ return []
173
230
  })
174
231
 
175
232
  // Sync breadcrumb and navlinks to AppLayout via provide/inject
176
- watch(breadcrumbItems, (items) => {
233
+ // Watch computed values + route changes + entity data to ensure updates
234
+ watch([breadcrumbItems, () => route.fullPath, breadcrumbEntities], ([items]) => {
177
235
  if (breadcrumbOverride) {
178
236
  breadcrumbOverride.value = items
179
237
  }
180
- }, { immediate: true })
238
+ }, { immediate: true, deep: true })
181
239
 
182
- watch(allNavlinks, (links) => {
240
+ watch([allNavlinks, () => route.fullPath], ([links]) => {
183
241
  if (navlinksOverride) {
184
242
  navlinksOverride.value = links
185
243
  }
186
244
  }, { immediate: true })
187
245
 
188
- // Clear overrides when component unmounts (so other pages get default breadcrumb)
189
- onUnmounted(() => {
190
- if (breadcrumbOverride) {
191
- breadcrumbOverride.value = null
192
- }
193
- if (navlinksOverride) {
194
- navlinksOverride.value = null
195
- }
196
- })
246
+ // Note: We intentionally do NOT clear overrides in onUnmounted.
247
+ // When navigating between routes, the new PageNav's watch will overwrite the values.
248
+ // Clearing in onUnmounted causes a race condition where the old PageNav clears
249
+ // AFTER the new PageNav has already set its values.
197
250
  </script>
198
251
 
199
252
  <template>
@@ -49,13 +49,21 @@ let alterationPromise = null
49
49
  const registry = {
50
50
  /**
51
51
  * Add routes for this module
52
- * @param {string} prefix - Path prefix for all routes (e.g., 'agents')
52
+ *
53
+ * CONVENTION: Entity item routes MUST use :id as param name
54
+ * - List route: 'books' → /books
55
+ * - Item route: 'books/:id' → /books/:id (MUST be :id, not :bookId or :uuid)
56
+ * - Child route: 'books/:id/reviews' → parent.param = 'id'
57
+ *
58
+ * This convention is required for PageNav, breadcrumbs, and navigation to work correctly.
59
+ *
60
+ * @param {string} prefix - Path prefix for all routes (e.g., 'books' or 'books/:id/reviews')
53
61
  * @param {Array} moduleRoutes - Route definitions with relative paths
54
62
  * @param {object} options - Route options
55
63
  * @param {string} [options.entity] - Entity name for permission checking
56
64
  * @param {object} [options.parent] - Parent entity config for child routes
57
65
  * @param {string} options.parent.entity - Parent entity name (e.g., 'books')
58
- * @param {string} options.parent.param - Route param for parent ID (e.g., 'bookId')
66
+ * @param {string} options.parent.param - Route param for parent ID (MUST be 'id')
59
67
  * @param {string} options.parent.foreignKey - Foreign key field (e.g., 'book_id')
60
68
  * @param {string} [options.parent.itemRoute] - Override parent item route (auto: parentEntity.routePrefix + '-edit')
61
69
  * @param {string} [options.label] - Label for navlinks (defaults to entity labelPlural)
@@ -341,6 +349,18 @@ export function getSiblingRoutes(parentEntity, parentParam) {
341
349
  })
342
350
  }
343
351
 
352
+ /**
353
+ * Get child routes (routes that have this entity as parent)
354
+ * @param {string} entityName - Entity name to find children for
355
+ * @returns {Array} Routes with this entity as parent
356
+ */
357
+ export function getChildRoutes(entityName) {
358
+ return routes.filter(route => {
359
+ const parent = route.meta?.parent
360
+ return parent?.entity === entityName
361
+ })
362
+ }
363
+
344
364
  /**
345
365
  * Check if a route belongs to a family
346
366
  */