@quazardous/qdadm 1.19.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.
Files changed (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,942 @@
1
+ /**
2
+ * useEntityItemFormPage - Unified procedural builder for CRUD form pages
3
+ *
4
+ * Provides a declarative/procedural API to build form pages with:
5
+ * - Mode detection (create vs edit from route params)
6
+ * - Auto-load entity data in edit mode
7
+ * - Auto-generate form fields from EntityManager.fields schema
8
+ * - Dirty state tracking for unsaved changes
9
+ * - Validation with schema-derived and custom validators
10
+ * - Permission-aware actions (save, delete)
11
+ * - FormPage component binding via props/events
12
+ *
13
+ * ## Basic Usage
14
+ *
15
+ * ```ts
16
+ * const form = useEntityItemFormPage({ entity: 'books' })
17
+ * form.addSaveAction()
18
+ * form.addDeleteAction()
19
+ *
20
+ * <FormPage v-bind="form.props.value" v-on="form.events">
21
+ * <template #fields>
22
+ * <FormField v-model="form.data.title" name="title" />
23
+ * </template>
24
+ * </FormPage>
25
+ * ```
26
+ */
27
+ import { ref, computed, watch, onMounted, onUnmounted, provide, type Ref } from 'vue'
28
+ import { useRouter, useRoute } from 'vue-router'
29
+ import { useConfirm } from 'primevue/useconfirm'
30
+ import { useDirtyState } from './useDirtyState'
31
+ import { useUnsavedChangesGuard, type GuardDialogState } from './useUnsavedChangesGuard'
32
+ import { useBreadcrumb } from './useBreadcrumb'
33
+ import {
34
+ useEntityItemPage,
35
+ type CreateContext,
36
+ } from './useEntityItemPage'
37
+ import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
38
+ import { deepClone } from '../utils/transformers'
39
+ import { getSiblingRoutes } from '../module/moduleRegistry'
40
+ import {
41
+ useFieldManager,
42
+ } from './useFieldManager'
43
+
44
+ // Import types from dedicated types file
45
+ import type {
46
+ Orchestrator,
47
+ ToastHelper,
48
+ ConfirmService,
49
+ AxiosError,
50
+ ErrorSummaryItem,
51
+ PageTitleParts,
52
+ } from './useEntityItemFormPage.types'
53
+ import { TYPE_MAPPINGS, TYPE_VALIDATORS, snakeCaseToTitle, isEmpty } from './useEntityItemFormPage.types'
54
+
55
+ // Re-export public types
56
+ export type {
57
+ FieldDefinition,
58
+ ResolvedFieldConfig,
59
+ ActionConfig,
60
+ ResolvedAction,
61
+ FormPageProps,
62
+ FormPageEvents,
63
+ UseEntityItemFormPageOptions,
64
+ UseEntityItemFormPageReturn,
65
+ GenerateFieldsOptions,
66
+ AddFieldOptions,
67
+ AddSaveActionOptions,
68
+ AddDeleteActionOptions,
69
+ AddCancelActionOptions,
70
+ } from './useEntityItemFormPage.types'
71
+
72
+ // Import concrete types needed in the function body
73
+ import type {
74
+ FieldDefinition,
75
+ ResolvedFieldConfig,
76
+ ActionConfig,
77
+ ResolvedAction,
78
+ FormPageProps,
79
+ FormPageEvents,
80
+ UseEntityItemFormPageOptions,
81
+ UseEntityItemFormPageReturn,
82
+ GenerateFieldsOptions,
83
+ AddSaveActionOptions,
84
+ AddDeleteActionOptions,
85
+ AddCancelActionOptions,
86
+ } from './useEntityItemFormPage.types'
87
+
88
+
89
+ /**
90
+ * Unified procedural builder for CRUD form pages
91
+ *
92
+ * @param config - Configuration options
93
+ * @returns Form builder API
94
+ */
95
+ export function useEntityItemFormPage<T extends Record<string, unknown> = Record<string, unknown>>(
96
+ config: UseEntityItemFormPageOptions<T>
97
+ ): UseEntityItemFormPageReturn<T> {
98
+ const {
99
+ entity,
100
+ // Mode detection
101
+ getId = null,
102
+ createRouteSuffix = 'create',
103
+ editRouteSuffix: _editRouteSuffix = 'edit', // eslint-disable-line @typescript-eslint/no-unused-vars
104
+ // Form options
105
+ loadOnMount = true,
106
+ enableGuard = true,
107
+ usePatch = false,
108
+ // Hooks for custom behavior
109
+ transformLoad = (data: unknown) => data,
110
+ transformSave = (data: unknown) => data,
111
+ onLoadSuccess = null,
112
+ onSaveSuccess = null,
113
+ onDeleteSuccess = null,
114
+ // Validation options
115
+ validateOnBlur = true,
116
+ validateOnSubmit = true,
117
+ showErrorSummary = false,
118
+ // Field generation
119
+ generateFormFields = true,
120
+ } = config
121
+
122
+ const router = useRouter()
123
+ const route = useRoute()
124
+ const confirm = useConfirm() as ConfirmService
125
+
126
+ // Use useEntityItemPage for common infrastructure
127
+ const itemPage = useEntityItemPage({
128
+ entity,
129
+ loadOnMount: false, // Form controls its own loading
130
+ getId,
131
+ })
132
+
133
+ const {
134
+ manager,
135
+ orchestrator,
136
+ entityId,
137
+ getInitialDataWithParent,
138
+ parentConfig,
139
+ parentId,
140
+ parentData,
141
+ parentChain,
142
+ getChainDepth,
143
+ hydrator,
144
+ } = itemPage
145
+
146
+ // Toast helper - wraps orchestrator.toast for legacy compatibility
147
+ const toast: ToastHelper = {
148
+ add({ severity, summary, detail, emitter }) {
149
+ ;(orchestrator as Orchestrator)?.toast?.[severity]?.(summary, detail, emitter)
150
+ },
151
+ }
152
+
153
+ // Read config from manager with option overrides
154
+ const entityName = config.entityName ?? manager.label
155
+ const routePrefix = config.routePrefix ?? manager.routePrefix
156
+
157
+ // Initial data: merge user-provided with auto-populated parent foreignKey
158
+ const baseInitialData = getInitialDataWithParent()
159
+ const initialData: Record<string, unknown> = config.initialData
160
+ ? { ...baseInitialData, ...config.initialData }
161
+ : baseInitialData
162
+
163
+ /**
164
+ * Detect form mode: 'create' or 'edit'
165
+ */
166
+ const mode = computed<'create' | 'edit'>(() => {
167
+ const routeName = (route.name as string) || ''
168
+ if (routeName.endsWith(createRouteSuffix) || routeName.endsWith('-new')) {
169
+ return 'create'
170
+ }
171
+ if (entityId.value) {
172
+ return 'edit'
173
+ }
174
+ return 'create'
175
+ })
176
+
177
+ const isEdit = computed(() => mode.value === 'edit')
178
+ const isCreate = computed(() => mode.value === 'create')
179
+
180
+ // ============ STATE ============
181
+
182
+ const data = ref(deepClone(initialData)) as Ref<T>
183
+ const originalData = ref<unknown>(null)
184
+ const loading = ref(false)
185
+ const saving = ref(false)
186
+
187
+ // Dirty state getter
188
+ const dirtyStateGetter = config.getDirtyState || (() => ({ form: data.value }))
189
+
190
+ // Dirty state tracking
191
+ const { dirty, dirtyFields, isFieldDirty, takeSnapshot, checkDirty } =
192
+ useDirtyState(dirtyStateGetter)
193
+
194
+ // Provide isFieldDirty and dirtyFields for child components (FormField)
195
+ provide('isFieldDirty', isFieldDirty)
196
+ provide('dirtyFields', dirtyFields)
197
+
198
+ // Watch for changes to update dirty state
199
+ watch(data, checkDirty, { deep: true })
200
+
201
+ // ============ VALIDATION STATE ============
202
+
203
+ const errors = ref<Record<string, string>>({})
204
+ const submitted = ref(false)
205
+ const hasErrors = computed(() => Object.keys(errors.value).length > 0)
206
+
207
+ const errorSummary = computed<ErrorSummaryItem[]>(() => {
208
+ return Object.entries(errors.value).map(([field, message]) => {
209
+ const fieldConfig = fieldsMap.value.get(field)
210
+ const label = fieldConfig?.label || snakeCaseToTitle(field)
211
+ return { field, label, message }
212
+ })
213
+ })
214
+
215
+ // Provide validation state for child components (FormField)
216
+ provide('getFieldError', (name: string) => errors.value[name] || null)
217
+ provide('formSubmitted', submitted)
218
+
219
+ // ============ UNSAVED CHANGES GUARD ============
220
+
221
+ let guardDialog: GuardDialogState | null = null
222
+ if (enableGuard) {
223
+ const { guardDialog: gd } = useUnsavedChangesGuard(dirty, {
224
+ onSave: async () => { await submit(false) },
225
+ })
226
+ guardDialog = gd
227
+ registerGuardDialog(guardDialog)
228
+ onUnmounted(() => unregisterGuardDialog(guardDialog!))
229
+ }
230
+
231
+ // ============ BREADCRUMB ============
232
+
233
+ const { breadcrumbItems } = useBreadcrumb({ entity: data })
234
+
235
+ // ============ LOADING ============
236
+
237
+ async function load(): Promise<void> {
238
+ if (!isEdit.value) {
239
+ data.value = deepClone(initialData) as T
240
+ takeSnapshot()
241
+ return
242
+ }
243
+
244
+ loading.value = true
245
+ try {
246
+ const responseData = await manager.get(entityId.value!)
247
+ const transformed = transformLoad(responseData)
248
+ data.value = transformed as T
249
+ originalData.value = deepClone(transformed)
250
+ takeSnapshot()
251
+
252
+ // Update active stack
253
+ hydrator.setCurrentData(transformed)
254
+
255
+ if (onLoadSuccess) {
256
+ await onLoadSuccess(transformed as T)
257
+ }
258
+ } catch (error) {
259
+ const axiosError = error as AxiosError
260
+ toast.add({
261
+ severity: 'error',
262
+ summary: 'Error',
263
+ detail: axiosError.response?.data?.detail || `Failed to load ${entityName}`,
264
+ life: 5000,
265
+ })
266
+ } finally {
267
+ loading.value = false
268
+ }
269
+ }
270
+
271
+ // ============ SUBMIT ============
272
+
273
+ async function submit(andClose = true): Promise<unknown> {
274
+ // Mark as submitted (shows all errors in UI)
275
+ submitted.value = true
276
+
277
+ // Validate before submit if enabled
278
+ if (validateOnSubmit && !validate()) {
279
+ toast.add({
280
+ severity: 'warn',
281
+ summary: 'Validation Error',
282
+ detail: 'Please fix the errors before saving',
283
+ life: 3000,
284
+ })
285
+ return null
286
+ }
287
+
288
+ saving.value = true
289
+ try {
290
+ const payload = transformSave(deepClone(data.value))
291
+
292
+ let responseData: unknown
293
+ if (isEdit.value) {
294
+ if (usePatch) {
295
+ responseData = await manager.patch(entityId.value!, payload)
296
+ } else {
297
+ responseData = await manager.update(entityId.value!, payload)
298
+ }
299
+ } else {
300
+ // Build context with parent chain for multi-storage routing
301
+ const context: CreateContext | undefined =
302
+ parentConfig.value && parentId.value
303
+ ? { parentChain: [{ entity: parentConfig.value.entity, id: parentId.value }] }
304
+ : undefined
305
+ responseData = await manager.create(payload, context)
306
+ }
307
+
308
+ toast.add({
309
+ severity: 'success',
310
+ summary: 'Success',
311
+ detail: `${entityName} ${isEdit.value ? 'updated' : 'created'} successfully`,
312
+ life: 3000,
313
+ })
314
+
315
+ // Update data and snapshot
316
+ const savedData = transformLoad(responseData)
317
+ data.value = savedData as T
318
+ originalData.value = deepClone(savedData)
319
+ takeSnapshot()
320
+
321
+ if (onSaveSuccess) {
322
+ await onSaveSuccess(responseData, andClose)
323
+ }
324
+
325
+ if (andClose) {
326
+ // Navigate to list route (or previous page)
327
+ const listRoute = findListRoute()
328
+ router.push(listRoute as { name: string })
329
+ } else if (!isEdit.value) {
330
+ // "Create" without close: navigate to edit route for the created entity
331
+ const createdId = (responseData as Record<string, unknown>)?.[manager.idField]
332
+ if (createdId) {
333
+ const currentRouteName = (route.name as string) || ''
334
+ const editRouteName = currentRouteName.replace(/(-create|-new)$/, '-edit')
335
+ router.push({
336
+ name: editRouteName,
337
+ params: { ...route.params, [manager.idField]: String(createdId) },
338
+ } as { name: string; params: Record<string, string> })
339
+ }
340
+ }
341
+
342
+ return responseData
343
+ } catch (error) {
344
+ const axiosError = error as AxiosError
345
+ toast.add({
346
+ severity: 'error',
347
+ summary: 'Error',
348
+ detail: axiosError.response?.data?.detail || `Failed to save ${entityName}`,
349
+ life: 5000,
350
+ })
351
+ throw error
352
+ } finally {
353
+ saving.value = false
354
+ }
355
+ }
356
+
357
+ // ============ DELETE ============
358
+
359
+ async function remove(): Promise<void> {
360
+ if (!isEdit.value) return
361
+
362
+ try {
363
+ await manager.delete(entityId.value!)
364
+ toast.add({
365
+ severity: 'success',
366
+ summary: 'Deleted',
367
+ detail: `${entityName} deleted successfully`,
368
+ life: 3000,
369
+ })
370
+
371
+ if (onDeleteSuccess) {
372
+ await onDeleteSuccess()
373
+ }
374
+
375
+ router.push({ name: routePrefix })
376
+ } catch (error) {
377
+ const axiosError = error as AxiosError
378
+ toast.add({
379
+ severity: 'error',
380
+ summary: 'Error',
381
+ detail: axiosError.response?.data?.detail || `Failed to delete ${entityName}`,
382
+ life: 5000,
383
+ })
384
+ }
385
+ }
386
+
387
+ function confirmDelete(): void {
388
+ const label = manager.getEntityLabel(data.value) || entityId.value
389
+ confirm.require({
390
+ message: `Delete ${entityName} "${label}"?`,
391
+ header: 'Confirm Delete',
392
+ icon: 'pi pi-exclamation-triangle',
393
+ acceptClass: 'p-button-danger',
394
+ accept: remove,
395
+ })
396
+ }
397
+
398
+ // ============ RESET ============
399
+
400
+ function reset(): void {
401
+ if (originalData.value) {
402
+ data.value = deepClone(originalData.value) as T
403
+ } else {
404
+ data.value = deepClone(initialData) as T
405
+ }
406
+ takeSnapshot()
407
+ // Clear validation state on reset
408
+ errors.value = {}
409
+ submitted.value = false
410
+ }
411
+
412
+ // ============ NAVIGATION ============
413
+
414
+ function findListRoute(): { name: string; params?: Record<string, unknown> } {
415
+ // If has parent config, find sibling list route
416
+ if (parentConfig.value) {
417
+ const { entity: parentEntityName, param } = parentConfig.value
418
+ const siblings = getSiblingRoutes(parentEntityName, param)
419
+
420
+ const currentRouteName = (route.name as string) || ''
421
+ const baseRouteName = currentRouteName.replace(/-(create|edit|new)$/, '')
422
+
423
+ const listRoutes = siblings.filter((r) => {
424
+ const name = (r.name as string) || ''
425
+ const path = (r.path as string) || ''
426
+ return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
427
+ })
428
+
429
+ let listRoute = listRoutes.find((r) => r.name === baseRouteName)
430
+
431
+ if (!listRoute && routePrefix) {
432
+ listRoute = listRoutes.find((r) => (r.name as string)?.includes(routePrefix))
433
+ }
434
+
435
+ if (!listRoute && listRoutes.length > 0) {
436
+ listRoute = listRoutes[0]
437
+ }
438
+
439
+ if (listRoute?.name) {
440
+ const params = { ...route.params } as Record<string, unknown>
441
+ delete params[manager.idField]
442
+ return { name: listRoute.name as string, params }
443
+ }
444
+ }
445
+
446
+ return { name: routePrefix! }
447
+ }
448
+
449
+ function cancel(): void {
450
+ router.push(findListRoute() as { name: string })
451
+ }
452
+
453
+ function goToList(): void {
454
+ router.push(findListRoute() as { name: string })
455
+ }
456
+
457
+ // ============ FIELDS & GROUPS (via useFieldManager) ============
458
+
459
+ /**
460
+ * Form-specific field resolver
461
+ */
462
+ function resolveFieldConfig(
463
+ name: string,
464
+ fieldConfig: Partial<FieldDefinition>
465
+ ): ResolvedFieldConfig {
466
+ const {
467
+ type = 'text',
468
+ label,
469
+ required = false,
470
+ default: defaultValue,
471
+ options,
472
+ optionLabel = 'label',
473
+ optionValue = 'value',
474
+ placeholder,
475
+ disabled = false,
476
+ readonly = false,
477
+ ...rest
478
+ } = fieldConfig
479
+
480
+ const inputType = TYPE_MAPPINGS[type] || 'text'
481
+ const resolvedLabel = label || snakeCaseToTitle(name)
482
+
483
+ return {
484
+ name,
485
+ type: inputType,
486
+ schemaType: type,
487
+ label: resolvedLabel,
488
+ required,
489
+ default: defaultValue,
490
+ options: options as unknown[] | undefined,
491
+ optionLabel,
492
+ optionValue,
493
+ placeholder,
494
+ disabled,
495
+ readonly,
496
+ ...rest,
497
+ }
498
+ }
499
+
500
+ // Use shared field manager
501
+ const fieldManager = useFieldManager<ResolvedFieldConfig>({
502
+ resolveFieldConfig,
503
+ getSchemaFieldConfig: (name) => manager.getFieldConfig(name) || null,
504
+ entity,
505
+ })
506
+
507
+ // Expose refs for validation (which needs direct access)
508
+ const { fieldsMap, fieldOrder, excludedFields, fields, groups } = fieldManager
509
+
510
+ /**
511
+ * Generate fields from manager schema
512
+ */
513
+ function generateFields(options: GenerateFieldsOptions = {}): UseEntityItemFormPageReturn<T> {
514
+ const formFields = manager.getFormFields()
515
+ fieldManager.generateFields(formFields, options)
516
+ // Auto-resolve reference options (async, non-blocking)
517
+ resolveReferences()
518
+ return builderApi
519
+ }
520
+
521
+ /**
522
+ * Resolve reference field options from related entities
523
+ */
524
+ async function resolveReferences(): Promise<void> {
525
+ for (const [name, config] of fieldsMap.value.entries()) {
526
+ if (config.options || !config.reference) continue
527
+
528
+ try {
529
+ const options = await manager.resolveReferenceOptions(name)
530
+ fieldManager.updateField(name, { options })
531
+ } catch (error) {
532
+ console.warn(
533
+ `[useEntityItemFormPage] Failed to resolve options for field '${name}':`,
534
+ error
535
+ )
536
+ }
537
+ }
538
+ }
539
+
540
+ // Chainable method wrapper (returns builderApi for fluent API)
541
+ const chain = <A extends unknown[], R>(fn: (...args: A) => R) =>
542
+ (...args: A): UseEntityItemFormPageReturn<T> => { fn(...args); return builderApi }
543
+
544
+ // Field management (chainable)
545
+ const addField = chain(fieldManager.addField)
546
+ const removeField = chain(fieldManager.removeField)
547
+ const excludeField = chain(fieldManager.excludeField)
548
+ const setFieldOrder = chain(fieldManager.setFieldOrder)
549
+ const moveField = chain(fieldManager.moveField)
550
+
551
+ // updateField needs custom validation logic
552
+ function updateField(name: string, fieldConfig: Partial<FieldDefinition>): UseEntityItemFormPageReturn<T> {
553
+ if (!fieldsMap.value.has(name)) {
554
+ throw new Error(`Field '${name}' does not exist. Use addField() to create new fields.`)
555
+ }
556
+ fieldManager.updateField(name, fieldConfig)
557
+ return builderApi
558
+ }
559
+
560
+ // Direct accessors
561
+ const getFieldConfig = fieldManager.getField
562
+ const getFields = (): ResolvedFieldConfig[] => fields.value
563
+
564
+ // Group management (chainable)
565
+ const group = chain(fieldManager.group)
566
+ const defineGroups = chain(fieldManager.defineGroups)
567
+
568
+ // Group accessors
569
+ const getGroup = fieldManager.getGroup
570
+ const getFieldsByGroup = fieldManager.getFieldsByGroup
571
+ const getUngroupedFields = fieldManager.getUngroupedFields
572
+
573
+ // ============ VALIDATION ============
574
+
575
+ function validateField(name: string): string | null {
576
+ const fieldConfig = fieldsMap.value.get(name)
577
+ if (!fieldConfig) return null
578
+
579
+ const value = data.value[name]
580
+
581
+ if (fieldConfig.required && isEmpty(value)) {
582
+ const error = `${fieldConfig.label} is required`
583
+ errors.value = { ...errors.value, [name]: error }
584
+ return error
585
+ }
586
+
587
+ const typeValidator = TYPE_VALIDATORS[fieldConfig.schemaType]
588
+ if (typeValidator) {
589
+ const result = typeValidator(value)
590
+ if (result !== true) {
591
+ errors.value = { ...errors.value, [name]: result as string }
592
+ return result as string
593
+ }
594
+ }
595
+
596
+ if (fieldConfig.validate && typeof fieldConfig.validate === 'function') {
597
+ const result = fieldConfig.validate(value, data.value)
598
+ if (result !== true && result !== undefined && result !== null) {
599
+ const error = typeof result === 'string' ? result : `${fieldConfig.label} is invalid`
600
+ errors.value = { ...errors.value, [name]: error }
601
+ return error
602
+ }
603
+ }
604
+
605
+ if (errors.value[name]) {
606
+ const { [name]: _removed, ...rest } = errors.value
607
+ void _removed // Intentionally unused - destructuring to omit
608
+ errors.value = rest
609
+ }
610
+
611
+ return null
612
+ }
613
+
614
+ function validate(): boolean {
615
+ const newErrors: Record<string, string> = {}
616
+
617
+ for (const fieldName of fieldOrder.value) {
618
+ const fieldConfig = fieldsMap.value.get(fieldName)
619
+ if (!fieldConfig) continue
620
+
621
+ const value = data.value[fieldName]
622
+
623
+ if (fieldConfig.required && isEmpty(value)) {
624
+ newErrors[fieldName] = `${fieldConfig.label} is required`
625
+ continue
626
+ }
627
+
628
+ const typeValidator = TYPE_VALIDATORS[fieldConfig.schemaType]
629
+ if (typeValidator) {
630
+ const result = typeValidator(value)
631
+ if (result !== true) {
632
+ newErrors[fieldName] = result as string
633
+ continue
634
+ }
635
+ }
636
+
637
+ if (fieldConfig.validate && typeof fieldConfig.validate === 'function') {
638
+ const result = fieldConfig.validate(value, data.value)
639
+ if (result !== true && result !== undefined && result !== null) {
640
+ newErrors[fieldName] =
641
+ typeof result === 'string' ? result : `${fieldConfig.label} is invalid`
642
+ }
643
+ }
644
+ }
645
+
646
+ errors.value = newErrors
647
+ return Object.keys(newErrors).length === 0
648
+ }
649
+
650
+ function clearErrors(): void {
651
+ errors.value = {}
652
+ submitted.value = false
653
+ }
654
+
655
+ function clearFieldError(name: string): void {
656
+ if (errors.value[name]) {
657
+ const { [name]: _removed, ...rest } = errors.value
658
+ void _removed // Intentionally unused - destructuring to omit
659
+ errors.value = rest
660
+ }
661
+ }
662
+
663
+ function getFieldError(name: string): string | null {
664
+ return errors.value[name] || null
665
+ }
666
+
667
+ function handleFieldBlur(name: string): void {
668
+ if (validateOnBlur) {
669
+ validateField(name)
670
+ }
671
+ }
672
+
673
+ provide('handleFieldBlur', handleFieldBlur)
674
+
675
+ // ============ PERMISSION STATE ============
676
+
677
+ const canSave = computed(() => {
678
+ return isEdit.value ? manager.canUpdate(data.value) : manager.canCreate()
679
+ })
680
+
681
+ const canDeleteRecord = computed(() => {
682
+ return isEdit.value && manager.canDelete(data.value)
683
+ })
684
+
685
+ // ============ ACTIONS ============
686
+
687
+ const actionsMap = ref<Map<string, ActionConfig>>(new Map())
688
+
689
+ function addAction(name: string, actionConfig: Omit<ActionConfig, 'name'>): void {
690
+ actionsMap.value.set(name, { name, ...actionConfig })
691
+ }
692
+
693
+ function removeAction(name: string): void {
694
+ actionsMap.value.delete(name)
695
+ }
696
+
697
+ function getActions(): ResolvedAction[] {
698
+ const actions: ResolvedAction[] = []
699
+ for (const [, action] of actionsMap.value) {
700
+ if (action.visible && !action.visible({ isEdit: isEdit.value, dirty: dirty.value })) continue
701
+ actions.push({
702
+ ...action,
703
+ isLoading: action.loading ? action.loading() : false,
704
+ isDisabled: action.disabled
705
+ ? action.disabled({ dirty: dirty.value, saving: saving.value })
706
+ : false,
707
+ })
708
+ }
709
+ return actions
710
+ }
711
+
712
+ const actions = computed(() => getActions())
713
+
714
+ function addSaveAction(options: AddSaveActionOptions = {}): void {
715
+ const { label, andClose = true } = options
716
+ const actionLabel = label || (andClose ? 'Save & Close' : 'Save')
717
+
718
+ addAction('save', {
719
+ label: actionLabel,
720
+ icon: andClose ? 'pi pi-check-circle' : 'pi pi-check',
721
+ severity: andClose ? 'success' : 'primary',
722
+ onClick: async () => { await submit(andClose) },
723
+ visible: () => (isEdit.value ? manager.canUpdate() : manager.canCreate()),
724
+ disabled: ({ dirty, saving }) => !dirty || saving,
725
+ loading: () => saving.value,
726
+ })
727
+ }
728
+
729
+ function addDeleteAction(options: AddDeleteActionOptions = {}): void {
730
+ const { label = 'Delete' } = options
731
+
732
+ addAction('delete', {
733
+ label,
734
+ icon: 'pi pi-trash',
735
+ severity: 'danger',
736
+ onClick: confirmDelete,
737
+ visible: () => isEdit.value && manager.canDelete(data.value),
738
+ })
739
+ }
740
+
741
+ function addCancelAction(options: AddCancelActionOptions = {}): void {
742
+ const { label = 'Cancel' } = options
743
+
744
+ addAction('cancel', {
745
+ label,
746
+ icon: 'pi pi-times',
747
+ severity: 'secondary',
748
+ onClick: cancel,
749
+ disabled: ({ saving }) => saving,
750
+ })
751
+ }
752
+
753
+ // ============ TITLE ============
754
+
755
+ const entityLabel = computed(() => {
756
+ return manager.getEntityLabel(data.value)
757
+ })
758
+
759
+ const pageTitle = computed(() => {
760
+ if (isEdit.value) {
761
+ const label = entityLabel.value
762
+ return label ? `Edit ${entityName}: ${label}` : `Edit ${entityName}`
763
+ }
764
+ return `Create ${entityName}`
765
+ })
766
+
767
+ const pageTitleParts = computed<PageTitleParts>(() => ({
768
+ action: isEdit.value ? 'Edit' : 'Create',
769
+ entityName,
770
+ entityLabel: isEdit.value ? entityLabel.value : undefined,
771
+ }))
772
+
773
+ provide('qdadmPageTitleParts', pageTitleParts)
774
+
775
+ // ============ LIFECYCLE ============
776
+
777
+ onMounted(() => {
778
+ if (loadOnMount) {
779
+ load()
780
+ }
781
+ })
782
+
783
+ watch(entityId, (newId, oldId) => {
784
+ if (newId !== oldId && loadOnMount) {
785
+ load()
786
+ }
787
+ })
788
+
789
+ // ============ FORMPAGE PROPS/EVENTS ============
790
+
791
+ const formProps = computed<FormPageProps>(() => ({
792
+ isEdit: isEdit.value,
793
+ mode: mode.value,
794
+ loading: loading.value,
795
+ saving: saving.value,
796
+ dirty: dirty.value,
797
+ title: pageTitle.value,
798
+ titleParts: pageTitleParts.value,
799
+ fields: fields.value,
800
+ groups: groups.value,
801
+ actions: actions.value,
802
+ canSave: canSave.value,
803
+ canDelete: canDeleteRecord.value,
804
+ errors: errors.value,
805
+ hasErrors: hasErrors.value,
806
+ errorSummary: showErrorSummary ? errorSummary.value : null,
807
+ submitted: submitted.value,
808
+ guardDialog,
809
+ }))
810
+
811
+ const formEvents: FormPageEvents = {
812
+ save: () => submit(false),
813
+ saveAndClose: () => submit(true),
814
+ cancel,
815
+ delete: confirmDelete,
816
+ }
817
+
818
+ // ============ BUILDER API ============
819
+
820
+ const builderApi: UseEntityItemFormPageReturn<T> = {
821
+ // Manager access
822
+ manager,
823
+
824
+ // Mode
825
+ mode,
826
+ isEdit,
827
+ isCreate,
828
+ entityId,
829
+
830
+ // Parent chain
831
+ parentConfig,
832
+ parentId,
833
+ parentData,
834
+ parentChain,
835
+ getChainDepth,
836
+
837
+ // State
838
+ data,
839
+ loading,
840
+ saving,
841
+ dirty,
842
+ dirtyFields,
843
+ originalData,
844
+
845
+ // Actions
846
+ load,
847
+ submit,
848
+ cancel,
849
+ remove,
850
+ confirmDelete,
851
+ reset,
852
+ goToList,
853
+ findListRoute,
854
+
855
+ // Dirty tracking
856
+ takeSnapshot,
857
+ checkDirty,
858
+ isFieldDirty,
859
+
860
+ // Field management
861
+ fields,
862
+ generateFields,
863
+ resolveReferences,
864
+ addField,
865
+ updateField,
866
+ removeField,
867
+ excludeField,
868
+ getFieldConfig,
869
+ getFields,
870
+ setFieldOrder,
871
+ moveField,
872
+
873
+ // Group management
874
+ groups,
875
+ group,
876
+ defineGroups,
877
+ getGroup,
878
+ getFieldsByGroup,
879
+ getUngroupedFields,
880
+
881
+ // Validation
882
+ errors,
883
+ hasErrors,
884
+ errorSummary,
885
+ submitted,
886
+ validate,
887
+ validateField,
888
+ clearErrors,
889
+ clearFieldError,
890
+ getFieldError,
891
+ handleFieldBlur,
892
+
893
+ // Action management
894
+ actions,
895
+ addAction,
896
+ removeAction,
897
+ getActions,
898
+ addSaveAction,
899
+ addDeleteAction,
900
+ addCancelAction,
901
+
902
+ // Permissions
903
+ canSave,
904
+ canDeleteRecord,
905
+
906
+ // Breadcrumb
907
+ breadcrumb: breadcrumbItems,
908
+
909
+ // Guard dialog
910
+ guardDialog,
911
+
912
+ // Title helpers
913
+ entityLabel,
914
+ pageTitle,
915
+ pageTitleParts,
916
+
917
+ // Utilities
918
+ toast,
919
+ confirm,
920
+ router,
921
+ route,
922
+
923
+ // FormPage integration
924
+ props: formProps,
925
+ events: formEvents,
926
+
927
+ // Stack hydrator
928
+ hydrator,
929
+ }
930
+
931
+ // Auto-generate fields from manager schema if enabled
932
+ if (generateFormFields) {
933
+ generateFields()
934
+ }
935
+
936
+ // Auto-disable parent foreignKey field (it's auto-filled from route)
937
+ if (parentConfig.value?.foreignKey && fieldsMap.value.has(parentConfig.value.foreignKey)) {
938
+ updateField(parentConfig.value.foreignKey, { disabled: true })
939
+ }
940
+
941
+ return builderApi
942
+ }