@quazardous/qdadm 1.21.0 → 2.0.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 +1 -1
- package/package.json +1 -1
- package/src/components/editors/VanillaJsonEditor.vue +42 -1
- package/src/components/editors/jsonValidator.ts +44 -0
- package/src/components/index.ts +0 -3
- package/src/components/layout/AppLayout.vue +2 -2
- package/src/components/layout/BaseLayout.vue +1 -1
- package/src/components/layout/PageHeader.vue +1 -1
- package/src/components/show/ShowDisplay.vue +6 -10
- package/src/composables/index.ts +0 -7
- package/src/composables/useListPage.ts +3 -8
- package/src/composables/useNavContext.ts +1 -1
- package/src/composables/usePageTitle.ts +2 -2
- package/src/entity/EntityManager.crud.ts +6 -1
- package/src/entity/storage/ApiStorage.ts +0 -5
- package/src/entity/storage/LocalStorage.ts +0 -5
- package/src/entity/storage/MemoryStorage.ts +0 -5
- package/src/entity/storage/MockApiStorage.ts +0 -5
- package/src/entity/storage/SdkStorage.ts +0 -5
- package/src/index.ts +0 -7
- package/src/utils/formatters.ts +38 -7
- package/src/utils/index.ts +1 -0
- package/src/components/edit/FormTab.vue +0 -217
- package/src/components/edit/FormTabs.vue +0 -120
- package/src/components/lists/ActionColumn.vue +0 -40
- package/src/composables/useCurrentEntity.ts +0 -60
- package/src/composables/useForm.ts +0 -544
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useForm - CRUD form composable extending useBareForm
|
|
3
|
-
*
|
|
4
|
-
* Provides standardized form handling:
|
|
5
|
-
* - Loading and saving states (via EntityManager)
|
|
6
|
-
* - Dirty state tracking (via useBareForm)
|
|
7
|
-
* - Unsaved changes guard (via useBareForm)
|
|
8
|
-
* - Toast notifications
|
|
9
|
-
* - Navigation helpers
|
|
10
|
-
* - form:alter hook for extensibility
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* ```ts
|
|
14
|
-
* // Minimal - reads config from EntityManager
|
|
15
|
-
* const { form, loading, saving, dirty, isEdit, submit, cancel } = useForm({
|
|
16
|
-
* entity: 'users',
|
|
17
|
-
* getId: () => route.params.id
|
|
18
|
-
* })
|
|
19
|
-
*
|
|
20
|
-
* // With overrides
|
|
21
|
-
* const { form, ... } = useForm({
|
|
22
|
-
* entity: 'users',
|
|
23
|
-
* getId: () => route.params.id,
|
|
24
|
-
* initialData: { name: '', email: '' }, // Override manager.getInitialData()
|
|
25
|
-
* routePrefix: 'user', // Override manager.routePrefix
|
|
26
|
-
* entityName: 'User' // Override manager.label
|
|
27
|
-
* })
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* ## form:alter Hook
|
|
31
|
-
*
|
|
32
|
-
* The composable invokes `form:alter` and `{entity}:form:alter` hooks after
|
|
33
|
-
* form initialization (load for edit, or initial data for create). This allows
|
|
34
|
-
* modules to modify form configuration dynamically.
|
|
35
|
-
*/
|
|
36
|
-
import { ref, computed, watch, onMounted, inject, provide, type Ref, type ComputedRef } from 'vue'
|
|
37
|
-
import { useBareForm } from './useBareForm'
|
|
38
|
-
import { useHooks } from './useHooks'
|
|
39
|
-
import { useStackHydrator } from '../chain/useStackHydrator'
|
|
40
|
-
import { deepClone } from '../utils/transformers'
|
|
41
|
-
import type { GuardDialogState } from './useUnsavedChangesGuard'
|
|
42
|
-
import type { BreadcrumbDisplayItem } from './useBreadcrumb'
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Entity manager interface
|
|
46
|
-
*/
|
|
47
|
-
interface EntityManager {
|
|
48
|
-
idField: string
|
|
49
|
-
label?: string
|
|
50
|
-
labelField?: string
|
|
51
|
-
routePrefix?: string
|
|
52
|
-
fields?: FieldDefinition[]
|
|
53
|
-
getInitialData: () => Record<string, unknown>
|
|
54
|
-
getFormFields?: () => FieldDefinition[]
|
|
55
|
-
getEntityLabel: (data: unknown) => string
|
|
56
|
-
get: (id: string | number) => Promise<unknown>
|
|
57
|
-
create: (data: unknown) => Promise<unknown>
|
|
58
|
-
update: (id: string | number, data: unknown) => Promise<unknown>
|
|
59
|
-
patch: (id: string | number, data: unknown) => Promise<unknown>
|
|
60
|
-
delete: (id: string | number) => Promise<void>
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Orchestrator interface
|
|
65
|
-
*/
|
|
66
|
-
interface Orchestrator {
|
|
67
|
-
get: (entityName: string) => EntityManager
|
|
68
|
-
toast: {
|
|
69
|
-
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
70
|
-
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
71
|
-
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
72
|
-
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Field definition interface
|
|
78
|
-
*/
|
|
79
|
-
interface FieldDefinition {
|
|
80
|
-
name: string
|
|
81
|
-
type?: string
|
|
82
|
-
label?: string
|
|
83
|
-
computed?: boolean
|
|
84
|
-
readonly?: boolean
|
|
85
|
-
[key: string]: unknown
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Form alter config passed to hooks
|
|
90
|
-
*/
|
|
91
|
-
export interface FormAlterConfig {
|
|
92
|
-
entity: string
|
|
93
|
-
fields: FieldDefinition[]
|
|
94
|
-
isEdit: boolean
|
|
95
|
-
entityId: string | number | null
|
|
96
|
-
form: unknown
|
|
97
|
-
manager: EntityManager
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Page title parts for PageHeader
|
|
102
|
-
*/
|
|
103
|
-
interface PageTitleParts {
|
|
104
|
-
action: string
|
|
105
|
-
entityName: string | undefined
|
|
106
|
-
entityLabel: string | null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Axios-like error interface
|
|
111
|
-
*/
|
|
112
|
-
interface AxiosError {
|
|
113
|
-
response?: {
|
|
114
|
-
data?: {
|
|
115
|
-
detail?: string
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
message?: string
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Options for useForm
|
|
123
|
-
*/
|
|
124
|
-
export interface UseFormOptions {
|
|
125
|
-
/** Entity name for EntityManager */
|
|
126
|
-
entity: string
|
|
127
|
-
/** Custom function to extract entity ID from route */
|
|
128
|
-
getId?: (() => string | number | null) | null
|
|
129
|
-
/** Transform loaded data before setting form */
|
|
130
|
-
transformLoad?: (data: unknown) => unknown
|
|
131
|
-
/** Transform form data before saving */
|
|
132
|
-
transformSave?: (data: unknown) => unknown
|
|
133
|
-
/** Callback on successful load */
|
|
134
|
-
onLoadSuccess?: ((data: unknown) => Promise<void> | void) | null
|
|
135
|
-
/** Callback on successful save */
|
|
136
|
-
onSaveSuccess?: ((data: unknown, andClose: boolean) => Promise<void> | void) | null
|
|
137
|
-
/** Enable unsaved changes guard (default: true) */
|
|
138
|
-
enableGuard?: boolean
|
|
139
|
-
/** Redirect to edit page after create (default: true) */
|
|
140
|
-
redirectOnCreate?: boolean
|
|
141
|
-
/** Custom dirty state getter */
|
|
142
|
-
getDirtyState?: (() => Record<string, unknown>) | null
|
|
143
|
-
/** Use PATCH instead of PUT for updates (default: false) */
|
|
144
|
-
usePatch?: boolean
|
|
145
|
-
/** Override manager.routePrefix */
|
|
146
|
-
routePrefix?: string
|
|
147
|
-
/** Override manager.label */
|
|
148
|
-
entityName?: string
|
|
149
|
-
/** Override manager.getInitialData() */
|
|
150
|
-
initialData?: Record<string, unknown>
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Return type for useForm
|
|
155
|
-
*/
|
|
156
|
-
export interface UseFormReturn {
|
|
157
|
-
// Manager access
|
|
158
|
-
manager: EntityManager
|
|
159
|
-
|
|
160
|
-
// State
|
|
161
|
-
form: Ref<unknown>
|
|
162
|
-
loading: Ref<boolean>
|
|
163
|
-
saving: Ref<boolean>
|
|
164
|
-
dirty: Ref<boolean>
|
|
165
|
-
dirtyFields: Ref<string[]>
|
|
166
|
-
isEdit: ComputedRef<boolean>
|
|
167
|
-
entityId: ComputedRef<string | number | null>
|
|
168
|
-
originalData: Ref<unknown>
|
|
169
|
-
|
|
170
|
-
// form:alter hook results
|
|
171
|
-
alteredFields: Ref<FieldDefinition[]>
|
|
172
|
-
hooksInvoked: Ref<boolean>
|
|
173
|
-
invokeFormAlterHook: () => Promise<void>
|
|
174
|
-
|
|
175
|
-
// Actions
|
|
176
|
-
load: () => Promise<void>
|
|
177
|
-
submit: (andClose?: boolean) => Promise<unknown>
|
|
178
|
-
cancel: () => void
|
|
179
|
-
remove: () => Promise<void>
|
|
180
|
-
reset: () => void
|
|
181
|
-
takeSnapshot: () => void
|
|
182
|
-
checkDirty: () => void
|
|
183
|
-
isFieldDirty: (fieldPath: string) => boolean
|
|
184
|
-
|
|
185
|
-
// Breadcrumb (auto-generated from route)
|
|
186
|
-
breadcrumb: ComputedRef<BreadcrumbDisplayItem[]>
|
|
187
|
-
|
|
188
|
-
// Guard dialog (for UnsavedChangesDialog - pass to PageLayout)
|
|
189
|
-
guardDialog: GuardDialogState | null
|
|
190
|
-
|
|
191
|
-
// Title helpers
|
|
192
|
-
entityLabel: ComputedRef<string>
|
|
193
|
-
pageTitle: ComputedRef<string>
|
|
194
|
-
pageTitleParts: ComputedRef<PageTitleParts>
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* CRUD form composable
|
|
199
|
-
*
|
|
200
|
-
* @param options - Configuration options
|
|
201
|
-
* @returns Form utilities and state
|
|
202
|
-
*/
|
|
203
|
-
export function useForm(options: UseFormOptions): UseFormReturn {
|
|
204
|
-
const {
|
|
205
|
-
entity,
|
|
206
|
-
getId = null,
|
|
207
|
-
// Callbacks
|
|
208
|
-
transformLoad = (data: unknown) => data,
|
|
209
|
-
transformSave = (data: unknown) => data,
|
|
210
|
-
onLoadSuccess = null,
|
|
211
|
-
onSaveSuccess = null,
|
|
212
|
-
// Options
|
|
213
|
-
enableGuard = true,
|
|
214
|
-
redirectOnCreate = true,
|
|
215
|
-
getDirtyState = null,
|
|
216
|
-
usePatch = false, // Use PATCH instead of PUT for updates
|
|
217
|
-
} = options
|
|
218
|
-
|
|
219
|
-
// Get EntityManager via orchestrator
|
|
220
|
-
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator')
|
|
221
|
-
if (!orchestrator) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
'[qdadm] Orchestrator not provided. Make sure to use createQdadm() with entityFactory.'
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
const manager = orchestrator.get(entity)
|
|
227
|
-
|
|
228
|
-
// Get HookRegistry for form:alter hook (optional, may not exist in tests)
|
|
229
|
-
const hooks = useHooks()
|
|
230
|
-
|
|
231
|
-
// Active stack for navigation context
|
|
232
|
-
const hydrator = useStackHydrator()
|
|
233
|
-
|
|
234
|
-
// Read config from manager with option overrides
|
|
235
|
-
const routePrefix = options.routePrefix ?? manager.routePrefix
|
|
236
|
-
const entityName = options.entityName ?? manager.label
|
|
237
|
-
const initialData = options.initialData ?? manager.getInitialData()
|
|
238
|
-
|
|
239
|
-
// Form-specific state
|
|
240
|
-
const form = ref<unknown>(deepClone(initialData))
|
|
241
|
-
const originalData = ref<unknown>(null)
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Altered fields configuration after form:alter hook processing
|
|
245
|
-
*
|
|
246
|
-
* Contains the field definitions modified by registered hooks.
|
|
247
|
-
* Modules can register form:alter hooks to modify field visibility,
|
|
248
|
-
* add/remove fields, change labels, or modify validation rules.
|
|
249
|
-
*/
|
|
250
|
-
const alteredFields = ref<FieldDefinition[]>([])
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Whether form:alter hooks have been invoked
|
|
254
|
-
*/
|
|
255
|
-
const hooksInvoked = ref(false)
|
|
256
|
-
|
|
257
|
-
// Dirty state getter
|
|
258
|
-
const dirtyStateGetter = getDirtyState || (() => ({ form: form.value }))
|
|
259
|
-
|
|
260
|
-
// Use base form for common functionality
|
|
261
|
-
const {
|
|
262
|
-
// Dependencies
|
|
263
|
-
router,
|
|
264
|
-
toast,
|
|
265
|
-
// State from useBareForm
|
|
266
|
-
loading,
|
|
267
|
-
saving,
|
|
268
|
-
dirty,
|
|
269
|
-
dirtyFields,
|
|
270
|
-
isEdit,
|
|
271
|
-
entityId,
|
|
272
|
-
// Dirty tracking
|
|
273
|
-
isFieldDirty,
|
|
274
|
-
takeSnapshot,
|
|
275
|
-
checkDirty,
|
|
276
|
-
// Helpers
|
|
277
|
-
cancel,
|
|
278
|
-
// Breadcrumb
|
|
279
|
-
breadcrumb,
|
|
280
|
-
// Guard dialog for unsaved changes
|
|
281
|
-
guardDialog,
|
|
282
|
-
} = useBareForm({
|
|
283
|
-
getState: dirtyStateGetter,
|
|
284
|
-
routePrefix,
|
|
285
|
-
guard: enableGuard,
|
|
286
|
-
onGuardSave: async () => { await submit(false) },
|
|
287
|
-
getId,
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
// Watch for changes
|
|
291
|
-
watch(form, checkDirty, { deep: true })
|
|
292
|
-
|
|
293
|
-
// ============ FORM:ALTER HOOK ============
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Invoke form:alter hooks to allow modules to modify form configuration
|
|
297
|
-
*
|
|
298
|
-
* Builds a config snapshot from current state, passes it through the hook chain,
|
|
299
|
-
* and stores the altered fields configuration.
|
|
300
|
-
*/
|
|
301
|
-
async function invokeFormAlterHook(): Promise<void> {
|
|
302
|
-
if (!hooks) return
|
|
303
|
-
|
|
304
|
-
// Get fields from manager (if available)
|
|
305
|
-
const managerFields =
|
|
306
|
-
typeof manager.getFormFields === 'function'
|
|
307
|
-
? manager.getFormFields()
|
|
308
|
-
: manager.fields || []
|
|
309
|
-
|
|
310
|
-
// Build config snapshot
|
|
311
|
-
const configSnapshot: FormAlterConfig = {
|
|
312
|
-
entity,
|
|
313
|
-
fields: deepClone(managerFields) as FieldDefinition[],
|
|
314
|
-
isEdit: isEdit.value,
|
|
315
|
-
entityId: entityId.value,
|
|
316
|
-
form: form.value,
|
|
317
|
-
manager,
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Invoke generic form:alter hook
|
|
321
|
-
let alteredConfig = (await hooks.alter('form:alter', configSnapshot)) as FormAlterConfig
|
|
322
|
-
|
|
323
|
-
// Invoke entity-specific hook: {entity}:form:alter
|
|
324
|
-
const entityHookName = `${entity}:form:alter`
|
|
325
|
-
if (hooks.hasHook(entityHookName)) {
|
|
326
|
-
alteredConfig = (await hooks.alter(entityHookName, alteredConfig)) as FormAlterConfig
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Store altered fields for consumption by FormPage
|
|
330
|
-
alteredFields.value = alteredConfig.fields || []
|
|
331
|
-
hooksInvoked.value = true
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Load entity
|
|
335
|
-
async function load(): Promise<void> {
|
|
336
|
-
if (!isEdit.value) {
|
|
337
|
-
form.value = deepClone(initialData)
|
|
338
|
-
takeSnapshot()
|
|
339
|
-
// Invoke form:alter hooks for create mode
|
|
340
|
-
await invokeFormAlterHook()
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
loading.value = true
|
|
345
|
-
try {
|
|
346
|
-
const responseData = await manager.get(entityId.value!)
|
|
347
|
-
const data = transformLoad(responseData)
|
|
348
|
-
form.value = data
|
|
349
|
-
originalData.value = deepClone(data)
|
|
350
|
-
takeSnapshot()
|
|
351
|
-
|
|
352
|
-
// Update active stack
|
|
353
|
-
hydrator.setCurrentData(data)
|
|
354
|
-
|
|
355
|
-
// Invoke form:alter hooks after data is loaded
|
|
356
|
-
await invokeFormAlterHook()
|
|
357
|
-
|
|
358
|
-
if (onLoadSuccess) {
|
|
359
|
-
await onLoadSuccess(data)
|
|
360
|
-
}
|
|
361
|
-
} catch (error) {
|
|
362
|
-
console.error(`Failed to load ${entityName}:`, error)
|
|
363
|
-
const axiosError = error as AxiosError
|
|
364
|
-
toast.add({
|
|
365
|
-
severity: 'error',
|
|
366
|
-
summary: 'Error',
|
|
367
|
-
detail: axiosError.response?.data?.detail || `Failed to load ${entityName}`,
|
|
368
|
-
})
|
|
369
|
-
} finally {
|
|
370
|
-
loading.value = false
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Submit form
|
|
375
|
-
async function submit(andClose = true): Promise<unknown> {
|
|
376
|
-
saving.value = true
|
|
377
|
-
try {
|
|
378
|
-
const payload = transformSave(deepClone(form.value))
|
|
379
|
-
|
|
380
|
-
let responseData: unknown
|
|
381
|
-
if (isEdit.value) {
|
|
382
|
-
if (usePatch) {
|
|
383
|
-
responseData = await manager.patch(entityId.value!, payload)
|
|
384
|
-
} else {
|
|
385
|
-
responseData = await manager.update(entityId.value!, payload)
|
|
386
|
-
}
|
|
387
|
-
} else {
|
|
388
|
-
responseData = await manager.create(payload)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
toast.add({
|
|
392
|
-
severity: 'success',
|
|
393
|
-
summary: 'Success',
|
|
394
|
-
detail: `${entityName} ${isEdit.value ? 'updated' : 'created'} successfully`,
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
// Update original data and snapshot
|
|
398
|
-
const savedData = transformLoad(responseData)
|
|
399
|
-
form.value = savedData
|
|
400
|
-
originalData.value = deepClone(savedData)
|
|
401
|
-
takeSnapshot()
|
|
402
|
-
|
|
403
|
-
if (onSaveSuccess) {
|
|
404
|
-
await onSaveSuccess(responseData, andClose)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (andClose) {
|
|
408
|
-
router.push({ name: routePrefix })
|
|
409
|
-
} else if (!isEdit.value && redirectOnCreate) {
|
|
410
|
-
// Redirect to edit mode after create
|
|
411
|
-
const newId = (responseData as Record<string, unknown>)[manager.idField]
|
|
412
|
-
router.replace({ name: `${routePrefix}-edit`, params: { id: newId as string } })
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return responseData
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.error(`Failed to save ${entityName}:`, error)
|
|
418
|
-
const axiosError = error as AxiosError
|
|
419
|
-
toast.add({
|
|
420
|
-
severity: 'error',
|
|
421
|
-
summary: 'Error',
|
|
422
|
-
detail: axiosError.response?.data?.detail || `Failed to save ${entityName}`,
|
|
423
|
-
})
|
|
424
|
-
throw error
|
|
425
|
-
} finally {
|
|
426
|
-
saving.value = false
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Delete entity
|
|
431
|
-
async function remove(): Promise<void> {
|
|
432
|
-
if (!isEdit.value) return
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
await manager.delete(entityId.value!)
|
|
436
|
-
toast.add({
|
|
437
|
-
severity: 'success',
|
|
438
|
-
summary: 'Success',
|
|
439
|
-
detail: `${entityName} deleted successfully`,
|
|
440
|
-
})
|
|
441
|
-
router.push({ name: routePrefix })
|
|
442
|
-
} catch (error) {
|
|
443
|
-
console.error(`Failed to delete ${entityName}:`, error)
|
|
444
|
-
const axiosError = error as AxiosError
|
|
445
|
-
toast.add({
|
|
446
|
-
severity: 'error',
|
|
447
|
-
summary: 'Error',
|
|
448
|
-
detail: axiosError.response?.data?.detail || `Failed to delete ${entityName}`,
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Reset form to original data
|
|
454
|
-
function reset(): void {
|
|
455
|
-
if (originalData.value) {
|
|
456
|
-
form.value = deepClone(originalData.value)
|
|
457
|
-
} else {
|
|
458
|
-
form.value = deepClone(initialData)
|
|
459
|
-
}
|
|
460
|
-
takeSnapshot()
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Load on mount if editing
|
|
464
|
-
onMounted(() => {
|
|
465
|
-
load()
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
// ============ COMPUTED TITLE ============
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Entity display label (e.g., "David Berlioz" for an agent)
|
|
472
|
-
* Uses manager.getEntityLabel() with labelField config
|
|
473
|
-
*/
|
|
474
|
-
const entityLabel = computed(() => {
|
|
475
|
-
return manager.getEntityLabel(form.value)
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Auto-generated page title
|
|
480
|
-
* - Edit mode: "Edit Agent: David Berlioz"
|
|
481
|
-
* - Create mode: "Create Agent"
|
|
482
|
-
*/
|
|
483
|
-
const pageTitle = computed(() => {
|
|
484
|
-
if (isEdit.value) {
|
|
485
|
-
const label = entityLabel.value
|
|
486
|
-
return label ? `Edit ${entityName}: ${label}` : `Edit ${entityName}`
|
|
487
|
-
}
|
|
488
|
-
return `Create ${entityName}`
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Structured page title for decorated rendering
|
|
493
|
-
* Returns { action, entityName, entityLabel } for custom styling
|
|
494
|
-
*/
|
|
495
|
-
const pageTitleParts = computed<PageTitleParts>(() => ({
|
|
496
|
-
action: isEdit.value ? 'Edit' : 'Create',
|
|
497
|
-
entityName,
|
|
498
|
-
entityLabel: isEdit.value ? entityLabel.value : null,
|
|
499
|
-
}))
|
|
500
|
-
|
|
501
|
-
// Provide title parts for automatic PageHeader consumption
|
|
502
|
-
provide('qdadmPageTitleParts', pageTitleParts)
|
|
503
|
-
|
|
504
|
-
return {
|
|
505
|
-
// Manager access
|
|
506
|
-
manager,
|
|
507
|
-
|
|
508
|
-
// State
|
|
509
|
-
form,
|
|
510
|
-
loading,
|
|
511
|
-
saving,
|
|
512
|
-
dirty,
|
|
513
|
-
dirtyFields,
|
|
514
|
-
isEdit,
|
|
515
|
-
entityId,
|
|
516
|
-
originalData,
|
|
517
|
-
|
|
518
|
-
// form:alter hook results
|
|
519
|
-
alteredFields,
|
|
520
|
-
hooksInvoked,
|
|
521
|
-
invokeFormAlterHook,
|
|
522
|
-
|
|
523
|
-
// Actions
|
|
524
|
-
load,
|
|
525
|
-
submit,
|
|
526
|
-
cancel,
|
|
527
|
-
remove,
|
|
528
|
-
reset,
|
|
529
|
-
takeSnapshot,
|
|
530
|
-
checkDirty,
|
|
531
|
-
isFieldDirty,
|
|
532
|
-
|
|
533
|
-
// Breadcrumb (auto-generated from route)
|
|
534
|
-
breadcrumb,
|
|
535
|
-
|
|
536
|
-
// Guard dialog (for UnsavedChangesDialog - pass to PageLayout)
|
|
537
|
-
guardDialog,
|
|
538
|
-
|
|
539
|
-
// Title helpers
|
|
540
|
-
entityLabel,
|
|
541
|
-
pageTitle,
|
|
542
|
-
pageTitleParts,
|
|
543
|
-
}
|
|
544
|
-
}
|