@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- 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
|
+
}
|