@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,692 @@
1
+ /**
2
+ * useFieldManager - Shared field and group management for form/show pages
3
+ *
4
+ * Provides unified API for:
5
+ * - Field management (add, update, remove, reorder)
6
+ * - Group management (hierarchical, with dot notation support)
7
+ * - Type mapping (customizable per consumer)
8
+ *
9
+ * ## Usage
10
+ *
11
+ * ```ts
12
+ * const fieldManager = useFieldManager({
13
+ * typeMappings: FORM_TYPE_MAPPINGS,
14
+ * resolveFieldConfig: (name, config) => ({ ... }),
15
+ * })
16
+ *
17
+ * fieldManager.generateFields(formFields, { exclude: ['id'] })
18
+ * fieldManager.group('info', ['name', 'email'], { label: 'Information' })
19
+ * ```
20
+ */
21
+ import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
22
+ import { useI18n } from '../i18n/useI18n'
23
+
24
+ /**
25
+ * Base field definition (minimal common interface)
26
+ */
27
+ export interface BaseFieldDefinition {
28
+ name: string
29
+ type?: string
30
+ schemaType?: string
31
+ label?: string
32
+ [key: string]: unknown
33
+ }
34
+
35
+ /**
36
+ * Resolved field configuration
37
+ */
38
+ export interface ResolvedFieldConfig extends BaseFieldDefinition {
39
+ name: string
40
+ type: string
41
+ schemaType: string
42
+ label: string
43
+ group?: string
44
+ }
45
+
46
+ /**
47
+ * Internal group representation
48
+ */
49
+ interface InternalGroup {
50
+ name: string
51
+ label: string
52
+ fields: string[]
53
+ parent?: string
54
+ // Tab/accordion options
55
+ icon?: string
56
+ badge?: string | number
57
+ badgeSeverity?: BadgeSeverity
58
+ count?: number
59
+ visible?: boolean
60
+ disabled?: boolean
61
+ }
62
+
63
+ /**
64
+ * Field group (tree structure for rendering)
65
+ */
66
+ export interface FieldGroup<T extends ResolvedFieldConfig = ResolvedFieldConfig> {
67
+ name: string
68
+ label: string
69
+ fields: T[]
70
+ children: FieldGroup<T>[]
71
+ parent?: string
72
+ // Tab/accordion options
73
+ icon?: string
74
+ badge?: string | number
75
+ badgeSeverity?: BadgeSeverity
76
+ count?: number
77
+ visible?: boolean
78
+ disabled?: boolean
79
+ }
80
+
81
+ /**
82
+ * Group definition input (for defineGroups)
83
+ */
84
+ export interface GroupDefinition extends GroupOptions {
85
+ fields?: string[]
86
+ children?: Record<string, GroupDefinition>
87
+ }
88
+
89
+ /**
90
+ * Badge severity type
91
+ */
92
+ export type BadgeSeverity = 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
93
+
94
+ /**
95
+ * Group options
96
+ */
97
+ export interface GroupOptions {
98
+ label?: string
99
+ /** Icon class (e.g., 'pi-cog', 'cog', 'pi pi-cog') */
100
+ icon?: string
101
+ /** Badge value */
102
+ badge?: string | number
103
+ /** Badge severity/color */
104
+ badgeSeverity?: BadgeSeverity
105
+ /** Count badge (shows only if > 0) */
106
+ count?: number
107
+ /** Show/hide the group (for tabs/accordion) */
108
+ visible?: boolean
109
+ /** Disable the group tab */
110
+ disabled?: boolean
111
+ }
112
+
113
+ /**
114
+ * Options for generateFields
115
+ */
116
+ export interface GenerateFieldsOptions {
117
+ /** Only include these fields */
118
+ only?: string[] | null
119
+ /** Exclude these fields */
120
+ exclude?: string[]
121
+ }
122
+
123
+ /**
124
+ * Options for addField
125
+ */
126
+ export interface AddFieldOptions {
127
+ /** Insert after this field */
128
+ after?: string | null
129
+ /** Insert before this field */
130
+ before?: string | null
131
+ }
132
+
133
+ /**
134
+ * Position options for moveField
135
+ */
136
+ export interface MoveFieldPosition {
137
+ /** Move after this field */
138
+ after?: string | null
139
+ /** Move before this field */
140
+ before?: string | null
141
+ }
142
+
143
+ /**
144
+ * Field resolver function
145
+ */
146
+ export type FieldResolver<T extends ResolvedFieldConfig = ResolvedFieldConfig> = (
147
+ name: string,
148
+ fieldConfig: Partial<BaseFieldDefinition>
149
+ ) => T
150
+
151
+ /**
152
+ * Options for useFieldManager
153
+ */
154
+ export interface UseFieldManagerOptions<T extends ResolvedFieldConfig = ResolvedFieldConfig> {
155
+ /** Custom field resolver (transforms raw config to resolved config) */
156
+ resolveFieldConfig?: FieldResolver<T>
157
+ /** Get field config from schema (e.g., manager.getFieldConfig) */
158
+ getSchemaFieldConfig?: (name: string) => Partial<BaseFieldDefinition> | null
159
+ /**
160
+ * Entity name. When set, field and group labels are resolved through
161
+ * `entities.{entity}.fields.{name}` / `entities.{entity}.groups.{name}` via
162
+ * i18n. The result wins over inline `label:` props if found; otherwise the
163
+ * inline label (then `snakeCaseToTitle`) is used. Required for the i18n
164
+ * forcing function — modules without an entity context fall back to legacy
165
+ * behaviour.
166
+ */
167
+ entity?: string
168
+ }
169
+
170
+ /**
171
+ * Return type for useFieldManager
172
+ */
173
+ export interface UseFieldManagerReturn<T extends ResolvedFieldConfig = ResolvedFieldConfig> {
174
+ // Field storage (refs for direct manipulation if needed)
175
+ fieldsMap: Ref<Map<string, T>>
176
+ fieldOrder: Ref<string[]>
177
+ excludedFields: Ref<Set<string>>
178
+
179
+ // Field management
180
+ fields: ComputedRef<T[]>
181
+ generateFields: (formFields: BaseFieldDefinition[], options?: GenerateFieldsOptions) => UseFieldManagerReturn<T>
182
+ addField: (name: string, fieldConfig: Partial<BaseFieldDefinition>, options?: AddFieldOptions) => UseFieldManagerReturn<T>
183
+ updateField: (name: string, updates: Partial<BaseFieldDefinition>) => UseFieldManagerReturn<T>
184
+ removeField: (name: string) => UseFieldManagerReturn<T>
185
+ excludeField: (name: string) => UseFieldManagerReturn<T>
186
+ setFieldOrder: (order: string[]) => UseFieldManagerReturn<T>
187
+ moveField: (name: string, position: MoveFieldPosition) => UseFieldManagerReturn<T>
188
+ getField: (name: string) => T | undefined
189
+ getFieldsByGroup: (groupName: string) => T[]
190
+
191
+ // Group management
192
+ groups: ComputedRef<FieldGroup<T>[]>
193
+ group: (name: string, fieldsOrOptions?: string[] | GroupOptions, options?: GroupOptions) => UseFieldManagerReturn<T>
194
+ defineGroups: (definitions: Record<string, GroupDefinition>) => UseFieldManagerReturn<T>
195
+ getGroup: (name: string) => FieldGroup<T> | undefined
196
+ clearGroups: () => UseFieldManagerReturn<T>
197
+
198
+ // Utilities
199
+ hasField: (name: string) => boolean
200
+ hasGroup: (name: string) => boolean
201
+ getUngroupedFields: () => T[]
202
+
203
+ /**
204
+ * Re-resolve labels for all fields and groups against the active locale.
205
+ * Called automatically when `i18n.locale` changes; exposed for manual
206
+ * triggering after asynchronous bundle loads.
207
+ */
208
+ relabel: () => void
209
+ }
210
+
211
+ /**
212
+ * Convert snake_case/kebab-case to Title Case
213
+ */
214
+ export function snakeCaseToTitle(str: string): string {
215
+ return str
216
+ .replace(/[-_]/g, ' ')
217
+ .replace(/\b\w/g, (char) => char.toUpperCase())
218
+ }
219
+
220
+ /**
221
+ * Default field resolver (basic type mapping)
222
+ */
223
+ function defaultResolveFieldConfig(
224
+ name: string,
225
+ fieldConfig: Partial<BaseFieldDefinition>
226
+ ): ResolvedFieldConfig {
227
+ const { type = 'text', label, ...rest } = fieldConfig
228
+ return {
229
+ name,
230
+ type,
231
+ schemaType: type,
232
+ label: label || snakeCaseToTitle(name),
233
+ ...rest,
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Create a field and group manager
239
+ */
240
+ export function useFieldManager<T extends ResolvedFieldConfig = ResolvedFieldConfig>(
241
+ options: UseFieldManagerOptions<T> = {}
242
+ ): UseFieldManagerReturn<T> {
243
+ const {
244
+ resolveFieldConfig: userResolveFieldConfig = defaultResolveFieldConfig as FieldResolver<T>,
245
+ getSchemaFieldConfig,
246
+ entity: entityName,
247
+ } = options
248
+
249
+ // i18n integration — looked up from the kernel injection. Returns a no-op
250
+ // shim if no kernel i18n is available (tests, isolated examples).
251
+ const { i18n, locale: i18nLocale } = useI18n()
252
+
253
+ /**
254
+ * Wrapped resolver: runs the user's resolver, then overlays the i18n label
255
+ * if the convention key resolves to a real translation. Tags the resolved
256
+ * config with `_labelKey` so relabel() can re-resolve on locale change.
257
+ */
258
+ function resolveFieldConfig(
259
+ name: string,
260
+ fieldConfig: Partial<BaseFieldDefinition>
261
+ ): T {
262
+ const resolved = userResolveFieldConfig(name, fieldConfig) as T & {
263
+ _labelKey?: string
264
+ _inlineLabel?: string
265
+ }
266
+ if (!entityName || !i18n) return resolved
267
+ const key = `entities.${entityName}.fields.${name}`
268
+ resolved._labelKey = key
269
+ // Preserve the inline label (if the user supplied one) so relabel() can
270
+ // fall back to it when the i18n bundle has no matching key.
271
+ resolved._inlineLabel = fieldConfig.label as string | undefined
272
+ const trace = i18n.resolve(key)
273
+ if (trace.hit) {
274
+ resolved.label = trace.result
275
+ }
276
+ return resolved
277
+ }
278
+
279
+ // ============ FIELD STORAGE ============
280
+
281
+ const fieldsMap = ref<Map<string, T>>(new Map()) as Ref<Map<string, T>>
282
+ const fieldOrder = ref<string[]>([])
283
+ const excludedFields = ref<Set<string>>(new Set())
284
+
285
+ // ============ FIELD MANAGEMENT ============
286
+
287
+ function generateFields(
288
+ formFields: BaseFieldDefinition[],
289
+ genOptions: GenerateFieldsOptions = {}
290
+ ): UseFieldManagerReturn<T> {
291
+ const { only = null, exclude = [] } = genOptions
292
+
293
+ const allExcluded = new Set([...excludedFields.value, ...exclude])
294
+
295
+ for (const fieldDef of formFields) {
296
+ const { name, ...fieldConfig } = fieldDef
297
+ if (!name) continue
298
+
299
+ if (only && !only.includes(name)) continue
300
+ if (allExcluded.has(name)) continue
301
+ if (fieldsMap.value.has(name)) continue
302
+
303
+ const resolvedConfig = resolveFieldConfig(name, fieldConfig)
304
+ fieldsMap.value.set(name, resolvedConfig)
305
+ fieldOrder.value.push(name)
306
+ }
307
+
308
+ return returnValue
309
+ }
310
+
311
+ function addField(
312
+ name: string,
313
+ fieldConfig: Partial<BaseFieldDefinition>,
314
+ addOptions: AddFieldOptions = {}
315
+ ): UseFieldManagerReturn<T> {
316
+ const { after = null, before = null } = addOptions
317
+
318
+ // Merge with schema config if available
319
+ const schemaConfig = getSchemaFieldConfig?.(name) || {}
320
+ const resolvedConfig = resolveFieldConfig(name, { ...schemaConfig, ...fieldConfig })
321
+
322
+ fieldsMap.value.set(name, resolvedConfig)
323
+
324
+ // Handle positioning
325
+ const currentIndex = fieldOrder.value.indexOf(name)
326
+ if (currentIndex !== -1) {
327
+ fieldOrder.value.splice(currentIndex, 1)
328
+ }
329
+
330
+ if (after) {
331
+ const afterIndex = fieldOrder.value.indexOf(after)
332
+ if (afterIndex !== -1) {
333
+ fieldOrder.value.splice(afterIndex + 1, 0, name)
334
+ } else {
335
+ fieldOrder.value.push(name)
336
+ }
337
+ } else if (before) {
338
+ const beforeIndex = fieldOrder.value.indexOf(before)
339
+ if (beforeIndex !== -1) {
340
+ fieldOrder.value.splice(beforeIndex, 0, name)
341
+ } else {
342
+ fieldOrder.value.unshift(name)
343
+ }
344
+ } else if (currentIndex === -1) {
345
+ fieldOrder.value.push(name)
346
+ } else {
347
+ fieldOrder.value.splice(currentIndex, 0, name)
348
+ }
349
+
350
+ return returnValue
351
+ }
352
+
353
+ function updateField(
354
+ name: string,
355
+ updates: Partial<BaseFieldDefinition>
356
+ ): UseFieldManagerReturn<T> {
357
+ const existing = fieldsMap.value.get(name)
358
+ if (existing) {
359
+ fieldsMap.value.set(name, { ...existing, ...updates } as T)
360
+ }
361
+ return returnValue
362
+ }
363
+
364
+ function removeField(name: string): UseFieldManagerReturn<T> {
365
+ fieldsMap.value.delete(name)
366
+ const idx = fieldOrder.value.indexOf(name)
367
+ if (idx !== -1) {
368
+ fieldOrder.value.splice(idx, 1)
369
+ }
370
+ return returnValue
371
+ }
372
+
373
+ function excludeField(name: string): UseFieldManagerReturn<T> {
374
+ excludedFields.value.add(name)
375
+ removeField(name)
376
+ return returnValue
377
+ }
378
+
379
+ function setFieldOrder(order: string[]): UseFieldManagerReturn<T> {
380
+ fieldOrder.value = order.filter((name) => fieldsMap.value.has(name))
381
+ return returnValue
382
+ }
383
+
384
+ function moveField(name: string, position: MoveFieldPosition): UseFieldManagerReturn<T> {
385
+ const { after = null, before = null } = position
386
+
387
+ const currentIndex = fieldOrder.value.indexOf(name)
388
+ if (currentIndex === -1) return returnValue
389
+
390
+ fieldOrder.value.splice(currentIndex, 1)
391
+
392
+ if (after) {
393
+ const afterIndex = fieldOrder.value.indexOf(after)
394
+ if (afterIndex !== -1) {
395
+ fieldOrder.value.splice(afterIndex + 1, 0, name)
396
+ } else {
397
+ fieldOrder.value.push(name)
398
+ }
399
+ } else if (before) {
400
+ const beforeIndex = fieldOrder.value.indexOf(before)
401
+ if (beforeIndex !== -1) {
402
+ fieldOrder.value.splice(beforeIndex, 0, name)
403
+ } else {
404
+ fieldOrder.value.unshift(name)
405
+ }
406
+ }
407
+
408
+ return returnValue
409
+ }
410
+
411
+ function getField(name: string): T | undefined {
412
+ return fieldsMap.value.get(name)
413
+ }
414
+
415
+ function hasField(name: string): boolean {
416
+ return fieldsMap.value.has(name)
417
+ }
418
+
419
+ const fields = computed<T[]>(() => {
420
+ return fieldOrder.value
421
+ .map((name) => fieldsMap.value.get(name))
422
+ .filter((f): f is T => f !== undefined)
423
+ })
424
+
425
+ // ============ GROUP MANAGEMENT ============
426
+
427
+ const groupsMap = ref<Map<string, InternalGroup>>(new Map())
428
+
429
+ /**
430
+ * Create or update a group
431
+ * @param name - Group name (supports dot notation: 'parent.child')
432
+ * @param fieldsOrOptions - Array of field names or group options
433
+ * @param options - Group options (if fieldsOrOptions is array)
434
+ */
435
+ function group(
436
+ name: string,
437
+ fieldsOrOptions?: string[] | GroupOptions,
438
+ options?: GroupOptions
439
+ ): UseFieldManagerReturn<T> {
440
+ const fieldNames = Array.isArray(fieldsOrOptions) ? fieldsOrOptions : []
441
+ const groupOptions = Array.isArray(fieldsOrOptions) ? options : fieldsOrOptions
442
+
443
+ // Parse parent from dot notation
444
+ const parts = name.split('.')
445
+ const groupName = parts[parts.length - 1] || name
446
+ const parentName = parts.length > 1 ? parts.slice(0, -1).join('.') : undefined
447
+
448
+ // Create parent groups if they don't exist
449
+ if (parentName && !groupsMap.value.has(parentName)) {
450
+ const parentLabel = parentName.split('.').pop() || parentName
451
+ group(parentName, { label: snakeCaseToTitle(parentLabel) })
452
+ }
453
+
454
+ // Create or update group
455
+ const existing = groupsMap.value.get(name)
456
+ // Resolve label: i18n hit (entities.X.groups.{name}) wins; fall back to
457
+ // explicit option, then existing label, then humanized form.
458
+ let resolvedLabel =
459
+ groupOptions?.label || existing?.label || snakeCaseToTitle(groupName)
460
+ if (entityName && i18n) {
461
+ const trace = i18n.resolve(`entities.${entityName}.groups.${name}`)
462
+ if (trace.hit) resolvedLabel = trace.result
463
+ }
464
+ groupsMap.value.set(name, {
465
+ name,
466
+ label: resolvedLabel,
467
+ fields: [...(existing?.fields || []), ...fieldNames],
468
+ parent: parentName,
469
+ // Tab/accordion options (preserve existing if not specified)
470
+ icon: groupOptions?.icon ?? existing?.icon,
471
+ badge: groupOptions?.badge ?? existing?.badge,
472
+ badgeSeverity: groupOptions?.badgeSeverity ?? existing?.badgeSeverity,
473
+ count: groupOptions?.count ?? existing?.count,
474
+ visible: groupOptions?.visible ?? existing?.visible,
475
+ disabled: groupOptions?.disabled ?? existing?.disabled,
476
+ })
477
+
478
+ // Update fields with group reference
479
+ for (const fieldName of fieldNames) {
480
+ const field = fieldsMap.value.get(fieldName)
481
+ if (field) {
482
+ fieldsMap.value.set(fieldName, { ...field, group: name } as T)
483
+ }
484
+ }
485
+
486
+ return returnValue
487
+ }
488
+
489
+ /**
490
+ * Define groups from object structure
491
+ */
492
+ function defineGroups(
493
+ definitions: Record<string, GroupDefinition>,
494
+ parentPath = ''
495
+ ): UseFieldManagerReturn<T> {
496
+ for (const [name, def] of Object.entries(definitions)) {
497
+ const fullPath = parentPath ? `${parentPath}.${name}` : name
498
+
499
+ // Extract options (everything except fields and children)
500
+ const { fields: defFields, children, ...options } = def
501
+
502
+ // Create group with fields and all options
503
+ group(fullPath, defFields || [], options)
504
+
505
+ // Recursively define children
506
+ if (children) {
507
+ defineGroups(children, fullPath)
508
+ }
509
+ }
510
+
511
+ return returnValue
512
+ }
513
+
514
+ /**
515
+ * Build group tree from internal group
516
+ */
517
+ function buildGroupTree(internal: InternalGroup): FieldGroup<T> {
518
+ const groupFields = internal.fields
519
+ .map((name) => fieldsMap.value.get(name))
520
+ .filter((f): f is T => f !== undefined)
521
+
522
+ // Find children
523
+ const children: FieldGroup<T>[] = []
524
+ for (const [, g] of groupsMap.value.entries()) {
525
+ if (g.parent === internal.name) {
526
+ children.push(buildGroupTree(g))
527
+ }
528
+ }
529
+
530
+ return {
531
+ name: internal.name,
532
+ label: internal.label,
533
+ fields: groupFields,
534
+ children,
535
+ parent: internal.parent,
536
+ // Tab/accordion options
537
+ icon: internal.icon,
538
+ badge: internal.badge,
539
+ badgeSeverity: internal.badgeSeverity,
540
+ count: internal.count,
541
+ visible: internal.visible,
542
+ disabled: internal.disabled,
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Get a group by name
548
+ */
549
+ function getGroup(name: string): FieldGroup<T> | undefined {
550
+ const internal = groupsMap.value.get(name)
551
+ if (!internal) return undefined
552
+ return buildGroupTree(internal)
553
+ }
554
+
555
+ function hasGroup(name: string): boolean {
556
+ return groupsMap.value.has(name)
557
+ }
558
+
559
+ function clearGroups(): UseFieldManagerReturn<T> {
560
+ // Remove group references from fields
561
+ for (const [name, field] of fieldsMap.value.entries()) {
562
+ if (field.group) {
563
+ fieldsMap.value.set(name, { ...field, group: undefined } as T)
564
+ }
565
+ }
566
+ groupsMap.value.clear()
567
+ return returnValue
568
+ }
569
+
570
+ /**
571
+ * Get fields belonging to a specific group
572
+ */
573
+ function getFieldsByGroup(groupName: string): T[] {
574
+ const internal = groupsMap.value.get(groupName)
575
+ if (!internal) return []
576
+
577
+ return internal.fields
578
+ .map((name) => fieldsMap.value.get(name))
579
+ .filter((f): f is T => f !== undefined)
580
+ }
581
+
582
+ /**
583
+ * Get fields that don't belong to any group
584
+ */
585
+ function getUngroupedFields(): T[] {
586
+ return fields.value.filter((f) => !f.group)
587
+ }
588
+
589
+ /**
590
+ * Computed groups tree (only root groups)
591
+ */
592
+ const groups = computed<FieldGroup<T>[]>(() => {
593
+ const rootGroups: FieldGroup<T>[] = []
594
+
595
+ for (const [, internal] of groupsMap.value.entries()) {
596
+ // Only include root groups (no parent)
597
+ if (!internal.parent) {
598
+ rootGroups.push(buildGroupTree(internal))
599
+ }
600
+ }
601
+
602
+ // If no groups defined, create a single "default" group with all fields
603
+ if (rootGroups.length === 0 && fields.value.length > 0) {
604
+ return [
605
+ {
606
+ name: '_default',
607
+ label: '',
608
+ fields: fields.value,
609
+ children: [],
610
+ },
611
+ ]
612
+ }
613
+
614
+ return rootGroups
615
+ })
616
+
617
+ // ============ I18N RELABEL ============
618
+
619
+ /**
620
+ * Re-resolve labels for all fields and groups against the active locale.
621
+ * Iterates the existing maps and replaces each entry's label using the
622
+ * stored convention key, falling back to the inline label or
623
+ * snakeCaseToTitle when the key has no translation.
624
+ */
625
+ function relabel(): void {
626
+ if (!entityName || !i18n) return
627
+
628
+ for (const [name, field] of fieldsMap.value.entries()) {
629
+ const meta = field as T & { _labelKey?: string; _inlineLabel?: string }
630
+ const key = meta._labelKey ?? `entities.${entityName}.fields.${name}`
631
+ const trace = i18n.resolve(key)
632
+ const newLabel = trace.hit
633
+ ? trace.result
634
+ : meta._inlineLabel || snakeCaseToTitle(name)
635
+ if (field.label !== newLabel) {
636
+ fieldsMap.value.set(name, { ...field, label: newLabel } as T)
637
+ }
638
+ }
639
+
640
+ for (const [name, internal] of groupsMap.value.entries()) {
641
+ const key = `entities.${entityName}.groups.${name}`
642
+ const trace = i18n.resolve(key)
643
+ if (trace.hit && internal.label !== trace.result) {
644
+ groupsMap.value.set(name, { ...internal, label: trace.result })
645
+ }
646
+ }
647
+ }
648
+
649
+ if (entityName && i18n) {
650
+ // Auto-relabel on locale change. Vue's watcher tracks the ref returned
651
+ // from useI18n(), so this fires for both immediate and async switches.
652
+ watch(i18nLocale, () => relabel())
653
+ }
654
+
655
+ // ============ RETURN ============
656
+
657
+ const returnValue: UseFieldManagerReturn<T> = {
658
+ // Field storage
659
+ fieldsMap,
660
+ fieldOrder,
661
+ excludedFields,
662
+
663
+ // Field management
664
+ fields,
665
+ generateFields,
666
+ addField,
667
+ updateField,
668
+ removeField,
669
+ excludeField,
670
+ setFieldOrder,
671
+ moveField,
672
+ getField,
673
+ getFieldsByGroup,
674
+
675
+ // Group management
676
+ groups,
677
+ group,
678
+ defineGroups,
679
+ getGroup,
680
+ clearGroups,
681
+
682
+ // Utilities
683
+ hasField,
684
+ hasGroup,
685
+ getUngroupedFields,
686
+
687
+ // i18n
688
+ relabel,
689
+ }
690
+
691
+ return returnValue
692
+ }