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
|
@@ -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
|
-
|
|
125
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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(
|
|
455
|
+
router.push(findListRoute())
|
|
392
456
|
}
|
|
393
457
|
|
|
394
458
|
function goToList() {
|
|
395
|
-
router.push(
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
326
|
-
const
|
|
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
|
|
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(
|
|
1193
|
+
router.push(findCreateRoute())
|
|
1133
1194
|
}
|
|
1134
1195
|
|
|
1135
1196
|
function goToEdit(item) {
|