qdadm 0.49.1 → 0.51.3

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.49.1",
3
+ "version": "0.51.3",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -31,7 +31,9 @@ const breadcrumbEntities = inject('qdadmBreadcrumbEntities', null)
31
31
 
32
32
  const props = defineProps({
33
33
  entity: { type: Object, default: null },
34
- parentEntity: { type: Object, default: null }
34
+ parentEntity: { type: Object, default: null },
35
+ // Show "Details" link in navlinks (default: false since breadcrumb shows current page)
36
+ showDetailsLink: { type: Boolean, default: false }
35
37
  })
36
38
 
37
39
  const route = useRoute()
@@ -121,8 +123,10 @@ const breadcrumbItems = computed(() => {
121
123
  })
122
124
 
123
125
  // Parent item (with label from data)
124
- const parentLabel = parentData.value
125
- ? parentManager.getEntityLabel(parentData.value)
126
+ // Prefer breadcrumbEntities (set by useEntityItemPage) over local parentData
127
+ const parentEntityData = breadcrumbEntities?.value?.get(1) || parentData.value
128
+ const parentLabel = parentEntityData
129
+ ? parentManager.getEntityLabel(parentEntityData)
126
130
  : '...'
127
131
  const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
128
132
  const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
@@ -149,8 +153,16 @@ const siblingNavlinks = computed(() => {
149
153
  const { entity: parentEntityName, param } = parentConfig.value
150
154
  const siblings = getSiblingRoutes(parentEntityName, param)
151
155
 
156
+ // Filter out action routes (create, edit) - they're not navigation tabs
157
+ const navRoutes = siblings.filter(r => {
158
+ const name = r.name || ''
159
+ const path = r.path || ''
160
+ // Exclude routes ending with -create, -edit, -new or paths with /create, /edit, /new
161
+ return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
162
+ })
163
+
152
164
  // Build navlinks with current route params
153
- return siblings.map(siblingRoute => {
165
+ return navRoutes.map(siblingRoute => {
154
166
  const manager = siblingRoute.meta?.entity ? getManager(siblingRoute.meta.entity) : null
155
167
  const label = siblingRoute.meta?.navLabel || manager?.labelPlural || siblingRoute.name
156
168
 
@@ -173,10 +185,19 @@ const childNavlinks = computed(() => {
173
185
  const children = getChildRoutes(entityName)
174
186
  if (children.length === 0) return []
175
187
 
188
+ // Filter out action routes (create, edit) - they're not navigation tabs
189
+ const navRoutes = children.filter(r => {
190
+ const name = r.name || ''
191
+ const path = r.path || ''
192
+ return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
193
+ })
194
+
195
+ if (navRoutes.length === 0) return []
196
+
176
197
  const entityId = route.params.id
177
198
 
178
199
  // Build navlinks to child routes
179
- return children.map(childRoute => {
200
+ return navRoutes.map(childRoute => {
180
201
  const childManager = childRoute.meta?.entity ? getManager(childRoute.meta.entity) : null
181
202
  const label = childRoute.meta?.navLabel || childManager?.labelPlural || childRoute.name
182
203
  const parentParam = childRoute.meta?.parent?.param || 'id'
@@ -191,7 +212,7 @@ const childNavlinks = computed(() => {
191
212
 
192
213
  // Combined navlinks with "Details" link
193
214
  const allNavlinks = computed(() => {
194
- // Case 1: On a child route - show siblings + Details link to parent
215
+ // Case 1: On a child route - show siblings + optional Details link to parent
195
216
  if (parentConfig.value) {
196
217
  const { entity: parentEntityName, param, itemRoute } = parentConfig.value
197
218
  const parentId = route.params[param]
@@ -199,31 +220,38 @@ const allNavlinks = computed(() => {
199
220
 
200
221
  if (!parentManager) return siblingNavlinks.value
201
222
 
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
223
+ // Details link is optional since breadcrumb already shows parent
224
+ if (props.showDetailsLink) {
225
+ const defaultSuffix = parentManager.readOnly ? '-show' : '-edit'
226
+ const parentRouteName = itemRoute || `${parentManager.routePrefix}${defaultSuffix}`
227
+ const isOnParentRoute = route.name === parentRouteName
228
+
229
+ // Details link to parent item page
230
+ // CONVENTION: Entity item routes MUST use :id as param name (e.g., /books/:id)
231
+ const detailsLink = {
232
+ label: 'Details',
233
+ to: { name: parentRouteName, params: { id: parentId } },
234
+ active: isOnParentRoute
235
+ }
236
+
237
+ return [detailsLink, ...siblingNavlinks.value]
213
238
  }
214
239
 
215
- return [detailsLink, ...siblingNavlinks.value]
240
+ return siblingNavlinks.value
216
241
  }
217
242
 
218
- // Case 2: On parent detail page - show children + Details (active)
243
+ // Case 2: On parent detail page - show children + optional Details (active)
219
244
  if (childNavlinks.value.length > 0) {
220
- const detailsLink = {
221
- label: 'Details',
222
- to: { name: route.name, params: route.params },
223
- active: true
245
+ // Details link is optional since breadcrumb already shows current page
246
+ if (props.showDetailsLink) {
247
+ const detailsLink = {
248
+ label: 'Details',
249
+ to: { name: route.name, params: route.params },
250
+ active: true
251
+ }
252
+ return [detailsLink, ...childNavlinks.value]
224
253
  }
225
-
226
- return [detailsLink, ...childNavlinks.value]
254
+ return childNavlinks.value
227
255
  }
228
256
 
229
257
  return []
@@ -87,6 +87,7 @@ import { useBreadcrumb } from './useBreadcrumb'
87
87
  import { useEntityItemPage } from './useEntityItemPage'
88
88
  import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
89
89
  import { deepClone } from '../utils/transformers'
90
+ import { getSiblingRoutes } from '../module/moduleRegistry'
90
91
 
91
92
  export function useEntityItemFormPage(config = {}) {
92
93
  const {
@@ -109,7 +110,9 @@ export function useEntityItemFormPage(config = {}) {
109
110
  // Validation options
110
111
  validateOnBlur = true, // Validate field on blur
111
112
  validateOnSubmit = true, // Validate all fields before submit
112
- showErrorSummary = false // Show error summary at top of form
113
+ showErrorSummary = false, // Show error summary at top of form
114
+ // Field generation
115
+ generateFormFields = true // Auto-generate fields from manager schema
113
116
  } = config
114
117
 
115
118
  const router = useRouter()
@@ -126,12 +129,18 @@ export function useEntityItemFormPage(config = {}) {
126
129
  getId
127
130
  })
128
131
 
129
- const { manager, orchestrator, entityId, setBreadcrumbEntity } = itemPage
132
+ const { manager, orchestrator, entityId, setBreadcrumbEntity, getInitialDataWithParent, parentConfig, parentId, parentData, parentChain, getChainDepth } = itemPage
130
133
 
131
134
  // Read config from manager with option overrides
132
135
  const entityName = config.entityName ?? manager.label
133
136
  const routePrefix = config.routePrefix ?? manager.routePrefix
134
- const initialData = config.initialData ?? manager.getInitialData()
137
+
138
+ // Initial data: merge user-provided with auto-populated parent foreignKey
139
+ // getInitialDataWithParent() adds the foreignKey from route.meta.parent
140
+ const baseInitialData = getInitialDataWithParent()
141
+ const initialData = config.initialData
142
+ ? { ...baseInitialData, ...config.initialData }
143
+ : baseInitialData
135
144
 
136
145
  /**
137
146
  * Detect form mode: 'create' or 'edit'
@@ -310,11 +319,19 @@ export function useEntityItemFormPage(config = {}) {
310
319
  }
311
320
 
312
321
  if (andClose) {
313
- router.push({ name: routePrefix })
322
+ // For child entities, redirect to sibling list route with parent params
323
+ const listRoute = findListRoute()
324
+ router.push(listRoute)
314
325
  } else if (!isEdit.value && redirectOnCreate) {
315
- // Redirect to edit mode after create
316
- const newId = responseData[manager.idField] || responseData.id || responseData.key
317
- router.replace({ name: `${routePrefix}-${editRouteSuffix}`, params: { id: newId } })
326
+ // Redirect to edit mode after create (only for top-level entities)
327
+ // Child entities with parent config go to list instead (edit route may not exist)
328
+ if (parentConfig.value) {
329
+ const listRoute = findListRoute()
330
+ router.replace(listRoute)
331
+ } else {
332
+ const newId = responseData[manager.idField] || responseData.id || responseData.key
333
+ router.replace({ name: `${routePrefix}-${editRouteSuffix}`, params: { id: newId } })
334
+ }
318
335
  }
319
336
 
320
337
  return responseData
@@ -387,12 +404,59 @@ export function useEntityItemFormPage(config = {}) {
387
404
 
388
405
  // ============ NAVIGATION ============
389
406
 
407
+ /**
408
+ * Find the list route for redirects after save/cancel
409
+ * For child entities: finds sibling list route with parent params
410
+ * For top-level entities: uses routePrefix
411
+ *
412
+ * When parent has multiple child entity types (e.g., bots → commands AND bots → logs),
413
+ * we find the list route matching the current entity's route prefix.
414
+ */
415
+ function findListRoute() {
416
+ // If has parent config, find sibling list route
417
+ if (parentConfig.value) {
418
+ const { entity: parentEntityName, param } = parentConfig.value
419
+ const siblings = getSiblingRoutes(parentEntityName, param)
420
+
421
+ // Extract base route name from current route (e.g., 'bot-commands-create' → 'bot-commands')
422
+ const currentRouteName = route.name || ''
423
+ const baseRouteName = currentRouteName.replace(/-(create|edit|new)$/, '')
424
+
425
+ // Find list routes among siblings (exclude create/edit/new routes)
426
+ const listRoutes = siblings.filter(r => {
427
+ const name = r.name || ''
428
+ const path = r.path || ''
429
+ return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
430
+ })
431
+
432
+ // Prefer route matching current entity's base name
433
+ let listRoute = listRoutes.find(r => r.name === baseRouteName)
434
+
435
+ // Fallback: try route containing routePrefix (e.g., 'bot-commands' contains 'command')
436
+ if (!listRoute && routePrefix) {
437
+ listRoute = listRoutes.find(r => r.name?.includes(routePrefix))
438
+ }
439
+
440
+ // Last fallback: first list route
441
+ if (!listRoute && listRoutes.length > 0) {
442
+ listRoute = listRoutes[0]
443
+ }
444
+
445
+ if (listRoute?.name) {
446
+ return { name: listRoute.name, params: route.params }
447
+ }
448
+ }
449
+
450
+ // Default: top-level entity list
451
+ return { name: routePrefix }
452
+ }
453
+
390
454
  function cancel() {
391
- router.push({ name: routePrefix })
455
+ router.push(findListRoute())
392
456
  }
393
457
 
394
458
  function goToList() {
395
- router.push({ name: routePrefix })
459
+ router.push(findListRoute())
396
460
  }
397
461
 
398
462
  // ============ FIELDS ============
@@ -592,8 +656,38 @@ export function useEntityItemFormPage(config = {}) {
592
656
  } else if (currentIndex === -1) {
593
657
  // New field, add at end
594
658
  fieldOrder.value.push(name)
659
+ } else {
660
+ // Existing field without repositioning, restore at original position
661
+ fieldOrder.value.splice(currentIndex, 0, name)
662
+ }
663
+
664
+ return builderApi
665
+ }
666
+
667
+ /**
668
+ * Update an existing field configuration
669
+ *
670
+ * Use this to modify properties of auto-generated fields.
671
+ * Throws error if field doesn't exist (use addField for new fields).
672
+ *
673
+ * @param {string} name - Field name to update
674
+ * @param {object} fieldConfig - Properties to merge with existing config
675
+ * @returns {object} - The builder instance for chaining
676
+ *
677
+ * @example
678
+ * form.updateField('book_id', { disabled: true })
679
+ * form.updateField('email', { validate: v => v.includes('@') || 'Invalid' })
680
+ */
681
+ function updateField(name, fieldConfig) {
682
+ if (!fieldsMap.value.has(name)) {
683
+ throw new Error(`Field '${name}' does not exist. Use addField() to create new fields.`)
595
684
  }
596
685
 
686
+ // Merge with existing config (keeps position)
687
+ const existingConfig = fieldsMap.value.get(name)
688
+ const mergedConfig = { ...existingConfig, ...fieldConfig }
689
+ fieldsMap.value.set(name, mergedConfig)
690
+
597
691
  return builderApi
598
692
  }
599
693
 
@@ -1094,6 +1188,13 @@ export function useEntityItemFormPage(config = {}) {
1094
1188
  isCreate,
1095
1189
  entityId,
1096
1190
 
1191
+ // Parent chain (from route.meta.parent, supports N-level nesting)
1192
+ parentConfig,
1193
+ parentId,
1194
+ parentData, // Immediate parent (level 1)
1195
+ parentChain, // All parents: Map(level -> data)
1196
+ getChainDepth,
1197
+
1097
1198
  // State
1098
1199
  data,
1099
1200
  loading,
@@ -1110,6 +1211,7 @@ export function useEntityItemFormPage(config = {}) {
1110
1211
  confirmDelete,
1111
1212
  reset,
1112
1213
  goToList,
1214
+ findListRoute,
1113
1215
 
1114
1216
  // Dirty tracking
1115
1217
  takeSnapshot,
@@ -1121,6 +1223,7 @@ export function useEntityItemFormPage(config = {}) {
1121
1223
  generateFields,
1122
1224
  resolveReferences,
1123
1225
  addField,
1226
+ updateField,
1124
1227
  removeField,
1125
1228
  excludeField,
1126
1229
  getFieldConfig,
@@ -1175,5 +1278,15 @@ export function useEntityItemFormPage(config = {}) {
1175
1278
  events: formEvents
1176
1279
  }
1177
1280
 
1281
+ // Auto-generate fields from manager schema if enabled
1282
+ if (generateFormFields) {
1283
+ generateFields()
1284
+ }
1285
+
1286
+ // Auto-disable parent foreignKey field (it's auto-filled from route)
1287
+ if (parentConfig.value?.foreignKey && fieldsMap.value.has(parentConfig.value.foreignKey)) {
1288
+ updateField(parentConfig.value.foreignKey, { disabled: true })
1289
+ }
1290
+
1178
1291
  return builderApi
1179
1292
  }
@@ -6,6 +6,7 @@
6
6
  * - Loading/error state management
7
7
  * - Breadcrumb integration (auto-calls setBreadcrumbEntity)
8
8
  * - Manager access for child composables
9
+ * - **Parent chain auto-detection** from route.meta.parent
9
10
  *
10
11
  * Used as base for:
11
12
  * - Show pages (read-only detail pages)
@@ -21,6 +22,21 @@
21
22
  * // error.value contains error message if failed
22
23
  * ```
23
24
  *
25
+ * ## Parent Chain Auto-Detection
26
+ *
27
+ * When route.meta.parent is configured, the composable auto-loads the parent entity:
28
+ *
29
+ * ```js
30
+ * // Route: /authors/:id/books/create
31
+ * // route.meta.parent = { entity: 'authors', param: 'id', foreignKey: 'author_id' }
32
+ *
33
+ * const { parentData, parentId, getInitialDataWithParent } = useEntityItemPage({ entity: 'books' })
34
+ *
35
+ * // parentData.value = loaded author entity
36
+ * // parentId.value = route.params.id
37
+ * // getInitialDataWithParent() = { ...defaults, author_id: parentId }
38
+ * ```
39
+ *
24
40
  * ## Usage in parent composables
25
41
  *
26
42
  * ```js
@@ -45,6 +61,8 @@ export function useEntityItemPage(config = {}) {
45
61
  // Loading options
46
62
  loadOnMount = true,
47
63
  breadcrumb = true,
64
+ // Parent chain options
65
+ autoLoadParent = true, // Auto-load parent entity from route.meta.parent
48
66
  // ID extraction
49
67
  getId = null,
50
68
  idParam = 'id',
@@ -82,6 +100,135 @@ export function useEntityItemPage(config = {}) {
82
100
  const loading = ref(false)
83
101
  const error = ref(null)
84
102
 
103
+ // ============ PARENT CHAIN ============
104
+
105
+ /**
106
+ * Parent configuration from route.meta.parent
107
+ * Structure: { entity, param, foreignKey, parent?: {...} }
108
+ * Supports nested parents for N-level chains
109
+ */
110
+ const parentConfig = computed(() => route.meta?.parent || null)
111
+
112
+ /**
113
+ * Parent entity ID from route params (based on parentConfig.param)
114
+ */
115
+ const parentId = computed(() => {
116
+ if (!parentConfig.value) return null
117
+ return route.params[parentConfig.value.param] || null
118
+ })
119
+
120
+ /**
121
+ * Loaded parent entities data (Map: level -> data)
122
+ * Level 1 = immediate parent, Level 2 = grandparent, etc.
123
+ */
124
+ const parentChain = ref(new Map())
125
+ const parentLoading = ref(false)
126
+
127
+ /**
128
+ * Immediate parent data (shortcut for level 1)
129
+ */
130
+ const parentData = computed(() => parentChain.value.get(1) || null)
131
+
132
+ /**
133
+ * Calculate the depth of the current entity in the parent chain
134
+ * Returns number of parent levels + 1 for current entity
135
+ */
136
+ function getChainDepth(config = parentConfig.value) {
137
+ if (!config) return 1
138
+ let depth = 1
139
+ let current = config
140
+ while (current) {
141
+ depth++
142
+ current = current.parent
143
+ }
144
+ return depth
145
+ }
146
+
147
+ /**
148
+ * Get parent entity manager (if parent is configured)
149
+ */
150
+ function getParentManager() {
151
+ if (!parentConfig.value) return null
152
+ return orchestrator.get(parentConfig.value.entity)
153
+ }
154
+
155
+ /**
156
+ * Load entire parent chain recursively
157
+ * Sets breadcrumb entities at correct levels (1 = top ancestor, N = immediate parent)
158
+ */
159
+ async function loadParentChain() {
160
+ if (!parentConfig.value || !parentId.value) return
161
+
162
+ parentLoading.value = true
163
+ const newChain = new Map()
164
+
165
+ try {
166
+ // Calculate total depth to set correct breadcrumb levels
167
+ // If chain is: grandparent -> parent -> current
168
+ // grandparent = level 1, parent = level 2, current = level 3
169
+ const totalDepth = getChainDepth()
170
+
171
+ // Load chain from immediate parent up to root
172
+ let currentConfig = parentConfig.value
173
+ let level = 1 // Level in our chain (1 = immediate parent)
174
+
175
+ while (currentConfig) {
176
+ const parentEntityId = route.params[currentConfig.param]
177
+ if (!parentEntityId) break
178
+
179
+ const parentManager = orchestrator.get(currentConfig.entity)
180
+ if (!parentManager) {
181
+ console.warn(`[useEntityItemPage] Parent manager not found: ${currentConfig.entity}`)
182
+ break
183
+ }
184
+
185
+ const data = await parentManager.get(parentEntityId)
186
+ if (data) {
187
+ newChain.set(level, data)
188
+
189
+ // Set breadcrumb at correct level
190
+ // breadcrumbLevel = totalDepth - level (so immediate parent is totalDepth-1, grandparent is totalDepth-2, etc.)
191
+ // Actually we want: root ancestor = 1, next = 2, ..., immediate parent = totalDepth-1, current = totalDepth
192
+ // So breadcrumbLevel = totalDepth - level
193
+ if (breadcrumb) {
194
+ const breadcrumbLevel = totalDepth - level
195
+ setBreadcrumbEntity(data, breadcrumbLevel)
196
+ }
197
+ }
198
+
199
+ currentConfig = currentConfig.parent
200
+ level++
201
+ }
202
+
203
+ parentChain.value = newChain
204
+ } catch (err) {
205
+ console.warn('[useEntityItemPage] Failed to load parent chain:', err)
206
+ } finally {
207
+ parentLoading.value = false
208
+ }
209
+ }
210
+
211
+ // Legacy alias for backwards compatibility
212
+ const loadParent = loadParentChain
213
+
214
+ /**
215
+ * Get initial data with parent foreignKey auto-populated
216
+ * Useful for form pages creating child entities
217
+ */
218
+ function getInitialDataWithParent() {
219
+ const baseData = manager.getInitialData()
220
+
221
+ if (!parentConfig.value || !parentId.value) {
222
+ return baseData
223
+ }
224
+
225
+ // Auto-populate foreignKey with parent ID
226
+ return {
227
+ ...baseData,
228
+ [parentConfig.value.foreignKey]: parentId.value
229
+ }
230
+ }
231
+
85
232
  // ============ ID EXTRACTION ============
86
233
 
87
234
  /**
@@ -121,8 +268,10 @@ export function useEntityItemPage(config = {}) {
121
268
  data.value = transformed
122
269
 
123
270
  // Share with navigation context for breadcrumb
271
+ // Level = depth of chain (1 if no parent, 2 if 1 parent, 3 if 2 parents, etc.)
124
272
  if (breadcrumb) {
125
- setBreadcrumbEntity(transformed)
273
+ const level = getChainDepth()
274
+ setBreadcrumbEntity(transformed, level)
126
275
  }
127
276
 
128
277
  if (onLoadSuccess) {
@@ -168,13 +317,17 @@ export function useEntityItemPage(config = {}) {
168
317
 
169
318
  // ============ LIFECYCLE ============
170
319
 
171
- if (loadOnMount) {
172
- onMounted(() => {
173
- if (entityId.value) {
174
- load()
175
- }
176
- })
177
- }
320
+ onMounted(async () => {
321
+ // Auto-load parent entity if configured
322
+ if (autoLoadParent && parentConfig.value && parentId.value) {
323
+ await loadParent()
324
+ }
325
+
326
+ // Load current entity
327
+ if (loadOnMount && entityId.value) {
328
+ load()
329
+ }
330
+ })
178
331
 
179
332
  // ============ RETURN ============
180
333
 
@@ -189,6 +342,18 @@ export function useEntityItemPage(config = {}) {
189
342
  entityLabel,
190
343
  isLoaded,
191
344
 
345
+ // Parent chain (supports N-level nesting)
346
+ parentConfig,
347
+ parentId,
348
+ parentData, // Immediate parent (level 1)
349
+ parentChain, // All parents: Map(level -> data)
350
+ parentLoading,
351
+ loadParent, // Alias for loadParentChain
352
+ loadParentChain,
353
+ getParentManager,
354
+ getChainDepth,
355
+ getInitialDataWithParent,
356
+
192
357
  // Actions
193
358
  load,
194
359
  reload,
@@ -321,13 +321,37 @@ export function useListPageBuilder(config = {}) {
321
321
  /**
322
322
  * Add standard "Create" header action
323
323
  * Respects manager.canCreate() for visibility
324
+ *
325
+ * @param {string|object} labelOrOptions - Label string or options object
326
+ * @param {string} labelOrOptions.label - Button label
327
+ * @param {string} labelOrOptions.routeName - Custom route name (overrides auto-detection)
328
+ * @param {object} labelOrOptions.params - Custom route params (merged with current params)
329
+ *
330
+ * @example
331
+ * // Simple usage
332
+ * list.addCreateAction('New Book')
333
+ *
334
+ * // With custom route (for child entities or special cases)
335
+ * list.addCreateAction({ label: 'New Command', routeName: 'bot-command-create' })
324
336
  */
325
- function addCreateAction(label = null) {
326
- const createLabel = label || `Create ${entityName.charAt(0).toUpperCase() + entityName.slice(1)}`
337
+ function addCreateAction(labelOrOptions = null) {
338
+ const options = typeof labelOrOptions === 'string'
339
+ ? { label: labelOrOptions }
340
+ : (labelOrOptions || {})
341
+
342
+ const createLabel = options.label || `Create ${entityName.charAt(0).toUpperCase() + entityName.slice(1)}`
343
+
344
+ const onClick = options.routeName
345
+ ? () => router.push({
346
+ name: options.routeName,
347
+ params: { ...route.params, ...options.params }
348
+ })
349
+ : goToCreate
350
+
327
351
  addHeaderAction('create', {
328
352
  label: createLabel,
329
353
  icon: 'pi pi-plus',
330
- onClick: goToCreate,
354
+ onClick,
331
355
  visible: () => manager.canCreate()
332
356
  })
333
357
  }
@@ -1128,8 +1152,45 @@ export function useListPageBuilder(config = {}) {
1128
1152
  }
1129
1153
 
1130
1154
  // ============ NAVIGATION ============
1155
+
1156
+ /**
1157
+ * Find create route for current context
1158
+ * For child routes (with parent config), looks for sibling create route
1159
+ * Falls back to standard {routePrefix}-create
1160
+ */
1161
+ function findCreateRoute() {
1162
+ const parentConfig = route.meta?.parent
1163
+
1164
+ if (parentConfig) {
1165
+ // Child route: find create route by path pattern
1166
+ // Current matched route path: /bots/:id/commands → look for /bots/:id/commands/create
1167
+ const currentMatched = route.matched[route.matched.length - 1]
1168
+ if (currentMatched) {
1169
+ const createPath = `${currentMatched.path}/create`
1170
+ const createRoute = router.getRoutes().find(r => r.path === createPath)
1171
+ if (createRoute?.name) {
1172
+ return { name: createRoute.name, params: route.params }
1173
+ }
1174
+ }
1175
+
1176
+ // Fallback: try common naming pattern (list-name → singular-create)
1177
+ // e.g., bot-commands → bot-command-create
1178
+ const currentName = route.name
1179
+ if (currentName && currentName.endsWith('s')) {
1180
+ const singularName = currentName.slice(0, -1)
1181
+ const createRouteName = `${singularName}-create`
1182
+ if (router.hasRoute(createRouteName)) {
1183
+ return { name: createRouteName, params: route.params }
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ // Default: standard entity route
1189
+ return { name: `${routePrefix}-create` }
1190
+ }
1191
+
1131
1192
  function goToCreate() {
1132
- router.push({ name: `${routePrefix}-create` })
1193
+ router.push(findCreateRoute())
1133
1194
  }
1134
1195
 
1135
1196
  function goToEdit(item) {