@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,158 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormInput - Auto-renders the appropriate input based on field config
4
+ *
5
+ * Takes a field config object and renders the matching PrimeVue component.
6
+ * Supports hint override but field.type takes precedence if specified.
7
+ *
8
+ * Usage:
9
+ * <FormInput :field="f" v-model="form.data.value[f.name]" />
10
+ */
11
+ import { computed, type PropType } from 'vue'
12
+ import InputText from 'primevue/inputtext'
13
+ import InputNumber from 'primevue/inputnumber'
14
+ import Textarea from 'primevue/textarea'
15
+ import Select from 'primevue/select'
16
+ import Checkbox from 'primevue/checkbox'
17
+ import Password from 'primevue/password'
18
+ import DatePicker from 'primevue/datepicker'
19
+
20
+ type InputType = 'text' | 'email' | 'password' | 'number' | 'textarea' | 'select' | 'boolean' | 'date' | 'datetime'
21
+ type ModelValue = string | number | boolean | Date | null
22
+
23
+ interface SelectOption {
24
+ label?: string
25
+ value?: string | number
26
+ [key: string]: unknown
27
+ }
28
+
29
+ interface FieldConfig {
30
+ name: string
31
+ type?: InputType
32
+ placeholder?: string
33
+ disabled?: boolean
34
+ readonly?: boolean
35
+ options?: SelectOption[]
36
+ optionLabel?: string
37
+ optionValue?: string
38
+ }
39
+
40
+ const props = defineProps({
41
+ field: { type: Object as PropType<FieldConfig>, required: true },
42
+ modelValue: { type: [String, Number, Boolean, Date, Object] as PropType<ModelValue>, default: null },
43
+ hint: { type: String as () => InputType | null, default: null } // Optional type hint override
44
+ })
45
+
46
+ const emit = defineEmits<{
47
+ 'update:modelValue': [value: ModelValue]
48
+ }>()
49
+
50
+ const value = computed<ModelValue>({
51
+ get: (): ModelValue => props.modelValue,
52
+ set: (v: ModelValue): void => emit('update:modelValue', v)
53
+ })
54
+
55
+ // Type-specific computed values for PrimeVue components
56
+ const stringValue = computed<string | null>({
57
+ get: (): string | null => (props.modelValue as string | null) ?? null,
58
+ set: (v: string | null): void => emit('update:modelValue', v)
59
+ })
60
+
61
+ const numberValue = computed<number | null>({
62
+ get: (): number | null => (props.modelValue as number | null) ?? null,
63
+ set: (v: number | null): void => emit('update:modelValue', v)
64
+ })
65
+
66
+ const booleanValue = computed<boolean>({
67
+ get: (): boolean => (props.modelValue as boolean) ?? false,
68
+ set: (v: boolean): void => emit('update:modelValue', v)
69
+ })
70
+
71
+ // Resolve component type: field.type > hint > 'text'
72
+ const inputType = computed<InputType>(() => props.field?.type || props.hint || 'text')
73
+
74
+ // Date value with string ↔ Date conversion for DatePicker
75
+ const dateValue = computed<Date | null>({
76
+ get: (): Date | null => {
77
+ const v = props.modelValue
78
+ if (!v) return null
79
+ if (v instanceof Date) return v
80
+ // Convert ISO string to Date
81
+ return new Date(v as string)
82
+ },
83
+ set: (v: Date | null): void => {
84
+ // Convert Date back to ISO string for storage
85
+ emit('update:modelValue', v ? v.toISOString() : null)
86
+ }
87
+ })
88
+ </script>
89
+
90
+ <template>
91
+ <InputText
92
+ v-if="inputType === 'text' || inputType === 'email'"
93
+ v-model="stringValue"
94
+ :placeholder="field.placeholder"
95
+ :disabled="field.disabled"
96
+ :readonly="field.readonly"
97
+ class="w-full"
98
+ />
99
+ <Password
100
+ v-else-if="inputType === 'password'"
101
+ v-model="stringValue"
102
+ :placeholder="field.placeholder"
103
+ :disabled="field.disabled"
104
+ :feedback="false"
105
+ toggleMask
106
+ class="w-full"
107
+ />
108
+ <InputNumber
109
+ v-else-if="inputType === 'number'"
110
+ v-model="numberValue"
111
+ :placeholder="field.placeholder"
112
+ :disabled="field.disabled"
113
+ :readonly="field.readonly"
114
+ :useGrouping="false"
115
+ class="w-full"
116
+ />
117
+ <Textarea
118
+ v-else-if="inputType === 'textarea'"
119
+ v-model="stringValue"
120
+ :placeholder="field.placeholder"
121
+ :disabled="field.disabled"
122
+ :readonly="field.readonly"
123
+ rows="3"
124
+ class="w-full"
125
+ />
126
+ <Select
127
+ v-else-if="inputType === 'select'"
128
+ v-model="value"
129
+ :options="field.options"
130
+ :optionLabel="field.optionLabel"
131
+ :optionValue="field.optionValue"
132
+ :placeholder="field.placeholder"
133
+ :disabled="field.disabled"
134
+ class="w-full"
135
+ />
136
+ <Checkbox
137
+ v-else-if="inputType === 'boolean'"
138
+ v-model="booleanValue"
139
+ :disabled="field.disabled"
140
+ binary
141
+ />
142
+ <DatePicker
143
+ v-else-if="inputType === 'date' || inputType === 'datetime'"
144
+ v-model="dateValue"
145
+ :placeholder="field.placeholder"
146
+ :disabled="field.disabled"
147
+ :showTime="inputType === 'datetime'"
148
+ class="w-full"
149
+ />
150
+ <!-- Fallback to text -->
151
+ <InputText
152
+ v-else
153
+ v-model="stringValue"
154
+ :placeholder="field.placeholder"
155
+ :disabled="field.disabled"
156
+ class="w-full"
157
+ />
158
+ </template>
@@ -0,0 +1,260 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormPage - Unified form page component
4
+ *
5
+ * Renders a complete CRUD form page with:
6
+ * - PageHeader with title and action buttons
7
+ * - Loading and error states
8
+ * - Form content via slots
9
+ * - FormActions footer
10
+ * - UnsavedChangesDialog integration
11
+ *
12
+ * Props come from useEntityItemFormPage composable:
13
+ *
14
+ * ```vue
15
+ * const form = useEntityItemFormPage({ entity: 'books' })
16
+ * form.generateFields()
17
+ * form.addSaveAction()
18
+ *
19
+ * <FormPage v-bind="form.props.value" v-on="form.events">
20
+ * <template #fields>
21
+ * <FormField v-model="form.data.title" name="title" />
22
+ * </template>
23
+ * </FormPage>
24
+ * ```
25
+ *
26
+ * Slots:
27
+ * - #nav: PageNav for breadcrumb customization
28
+ * - #toolbar: Custom toolbar actions (between header and form)
29
+ * - #header-actions: Custom header action buttons
30
+ * - #fields: Form fields content (required)
31
+ * - #footer: Custom footer (replaces FormActions if provided)
32
+ * - #error: Custom error display
33
+ * - #loading: Custom loading display
34
+ */
35
+ import { computed, type PropType } from 'vue'
36
+ import PageHeader from '../layout/PageHeader.vue'
37
+ import FormActions from './FormActions.vue'
38
+ import UnsavedChangesDialog from '../dialogs/UnsavedChangesDialog.vue'
39
+ import Card from 'primevue/card'
40
+ import Button from 'primevue/button'
41
+ import Message from 'primevue/message'
42
+ import type { ResolvedAction, ResolvedFieldConfig } from '../../composables/useEntityItemFormPage'
43
+ import type { GuardDialogState } from '../../composables/useUnsavedChangesGuard'
44
+
45
+ /**
46
+ * Page title parts for PageHeader
47
+ */
48
+ interface PageTitleParts {
49
+ action: string
50
+ entityName: string | undefined
51
+ entityLabel: string | undefined
52
+ }
53
+
54
+ /**
55
+ * Error summary item
56
+ */
57
+ interface ErrorSummaryItem {
58
+ field: string
59
+ label: string
60
+ message: string
61
+ }
62
+
63
+ /**
64
+ * Fetch error type
65
+ */
66
+ interface FetchError {
67
+ message?: string
68
+ detail?: string
69
+ }
70
+
71
+ const props = defineProps({
72
+ // Mode
73
+ isEdit: { type: Boolean, default: false },
74
+ mode: { type: String as PropType<'create' | 'edit'>, default: 'create' },
75
+
76
+ // State
77
+ loading: { type: Boolean, default: false },
78
+ saving: { type: Boolean, default: false },
79
+ dirty: { type: Boolean, default: false },
80
+
81
+ // Title (use title OR titleParts)
82
+ title: { type: String as PropType<string | null>, default: null },
83
+ titleParts: { type: Object as PropType<PageTitleParts | null>, default: null },
84
+
85
+ // Fields (for auto-rendering - optional, can use #fields slot instead)
86
+ fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
87
+
88
+ // Actions (from builder.actions)
89
+ actions: { type: Array as PropType<ResolvedAction[]>, default: () => [] },
90
+
91
+ // Validation state
92
+ errors: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
93
+ hasErrors: { type: Boolean, default: false },
94
+ errorSummary: { type: Array as PropType<ErrorSummaryItem[] | null>, default: null },
95
+ submitted: { type: Boolean, default: false },
96
+
97
+ // Guard dialog (from useUnsavedChangesGuard)
98
+ guardDialog: { type: Object as PropType<GuardDialogState | null>, default: null },
99
+
100
+ // Error for fetch failures (separate from validation errors)
101
+ fetchError: { type: [String, Object] as PropType<string | FetchError | null>, default: null },
102
+
103
+ // UI options
104
+ showFormActions: { type: Boolean, default: true },
105
+ showSaveAndClose: { type: Boolean, default: true },
106
+ cardWrapper: { type: Boolean, default: true }
107
+ })
108
+
109
+ const emit = defineEmits<{
110
+ (e: 'save'): void
111
+ (e: 'saveAndClose'): void
112
+ (e: 'cancel'): void
113
+ (e: 'delete'): void
114
+ }>()
115
+
116
+ // Header actions: all actions except save, delete, cancel (those go in footer)
117
+ const headerActions = computed<ResolvedAction[]>(() =>
118
+ props.actions.filter((a: ResolvedAction) => !['save', 'delete', 'cancel'].includes(a.name))
119
+ )
120
+
121
+ // Get error message from fetchError
122
+ const fetchErrorMessage = computed<string | null>(() => {
123
+ if (!props.fetchError) return null
124
+ if (typeof props.fetchError === 'string') return props.fetchError
125
+ return props.fetchError.message || props.fetchError.detail || 'Failed to load entity'
126
+ })
127
+
128
+ // Guard dialog handlers
129
+ function onGuardSaveAndLeave(): void {
130
+ if (props.guardDialog?.onSaveAndLeave) {
131
+ props.guardDialog.onSaveAndLeave()
132
+ }
133
+ }
134
+
135
+ function onGuardLeave(): void {
136
+ if (props.guardDialog?.onLeave) {
137
+ props.guardDialog.onLeave()
138
+ }
139
+ }
140
+
141
+ function onGuardStay(): void {
142
+ if (props.guardDialog?.onStay) {
143
+ props.guardDialog.onStay()
144
+ }
145
+ }
146
+ </script>
147
+
148
+ <template>
149
+ <div class="form-page">
150
+ <!-- Nav slot for PageNav (child routes) -->
151
+ <slot name="nav" />
152
+
153
+ <PageHeader :title="title" :title-parts="titleParts">
154
+ <template #actions>
155
+ <slot name="header-actions" />
156
+ <!-- Header actions from builder (excludes save/delete/cancel) -->
157
+ <Button
158
+ v-for="action in headerActions"
159
+ :key="action.name"
160
+ :label="action.label"
161
+ :icon="action.icon"
162
+ :severity="action.severity"
163
+ :loading="action.isLoading"
164
+ :disabled="action.isDisabled"
165
+ @click="action.onClick"
166
+ />
167
+ </template>
168
+ </PageHeader>
169
+
170
+ <!-- Toolbar slot (between header and form) -->
171
+ <slot name="toolbar" />
172
+
173
+ <!-- Loading State -->
174
+ <template v-if="loading">
175
+ <slot name="loading">
176
+ <div class="loading-state">
177
+ <i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
178
+ </div>
179
+ </slot>
180
+ </template>
181
+
182
+ <!-- Error State (fetch error) -->
183
+ <template v-else-if="fetchError">
184
+ <slot name="error" :error="fetchError">
185
+ <Message severity="error" :closable="false" class="form-error-message">
186
+ {{ fetchErrorMessage }}
187
+ </Message>
188
+ </slot>
189
+ </template>
190
+
191
+ <!-- Form Content -->
192
+ <template v-else>
193
+ <!-- Validation Error Summary -->
194
+ <Message
195
+ v-if="errorSummary && errorSummary.length > 0"
196
+ severity="warn"
197
+ :closable="false"
198
+ class="validation-summary"
199
+ >
200
+ <ul class="validation-errors">
201
+ <li v-for="error in errorSummary" :key="error.field">
202
+ <strong>{{ error.label }}:</strong> {{ error.message }}
203
+ </li>
204
+ </ul>
205
+ </Message>
206
+
207
+ <!-- Card wrapper or direct content -->
208
+ <Card v-if="cardWrapper">
209
+ <template #content>
210
+ <slot name="fields" />
211
+
212
+ <!-- Form Actions (in footer) -->
213
+ <template v-if="showFormActions">
214
+ <slot name="footer">
215
+ <FormActions
216
+ :isEdit="isEdit"
217
+ :saving="saving"
218
+ :dirty="dirty"
219
+ :showSaveAndClose="showSaveAndClose"
220
+ @save="emit('save')"
221
+ @saveAndClose="emit('saveAndClose')"
222
+ @cancel="emit('cancel')"
223
+ />
224
+ </slot>
225
+ </template>
226
+ </template>
227
+ </Card>
228
+
229
+ <template v-else>
230
+ <slot name="fields" />
231
+
232
+ <!-- Form Actions (in footer) -->
233
+ <template v-if="showFormActions">
234
+ <slot name="footer">
235
+ <FormActions
236
+ :isEdit="isEdit"
237
+ :saving="saving"
238
+ :dirty="dirty"
239
+ :showSaveAndClose="showSaveAndClose"
240
+ @save="emit('save')"
241
+ @saveAndClose="emit('saveAndClose')"
242
+ @cancel="emit('cancel')"
243
+ />
244
+ </slot>
245
+ </template>
246
+ </template>
247
+ </template>
248
+
249
+ <!-- Unsaved Changes Dialog -->
250
+ <UnsavedChangesDialog
251
+ v-if="guardDialog"
252
+ :visible="guardDialog.visible.value"
253
+ :saving="saving"
254
+ :hasOnSave="guardDialog.hasOnSave"
255
+ @saveAndLeave="onGuardSaveAndLeave"
256
+ @leave="onGuardLeave"
257
+ @stay="onGuardStay"
258
+ />
259
+ </div>
260
+ </template>
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormTab - Normalized tab header component
4
+ *
5
+ * @deprecated Use FieldGroups with layout="tabs" instead. FieldGroups now supports
6
+ * icon, badge, count, visible, and disabled options on groups.
7
+ *
8
+ * ```ts
9
+ * // Before (deprecated)
10
+ * <FormTab value="general" label="General" icon="pi-cog" :count="5" />
11
+ *
12
+ * // After (recommended)
13
+ * form.group('general', ['field1'], { label: 'General', icon: 'cog', count: 5 })
14
+ * ```
15
+ */
16
+
17
+ import { type PropType } from 'vue'
18
+ import Tab from 'primevue/tab'
19
+ import Tag from 'primevue/tag'
20
+
21
+ type BadgeSeverity = 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
22
+
23
+ const props = defineProps({
24
+ /**
25
+ * Tab identifier (used for v-model matching)
26
+ */
27
+ value: {
28
+ type: String,
29
+ required: true
30
+ },
31
+
32
+ /**
33
+ * Display label
34
+ */
35
+ label: {
36
+ type: String,
37
+ required: true
38
+ },
39
+
40
+ /**
41
+ * PrimeIcon class (without 'pi' prefix or with full class)
42
+ * Examples: 'pi-cog', 'cog', 'pi pi-cog'
43
+ */
44
+ icon: {
45
+ type: String as () => string | null,
46
+ default: null
47
+ },
48
+
49
+ /**
50
+ * Count to display as badge (shows only if > 0)
51
+ */
52
+ count: {
53
+ type: Number as () => number | null,
54
+ default: null
55
+ },
56
+
57
+ /**
58
+ * Custom badge text (alternative to count)
59
+ */
60
+ badge: {
61
+ type: [String, Number] as unknown as PropType<string | number | null>,
62
+ default: null
63
+ },
64
+
65
+ /**
66
+ * Badge severity/color
67
+ */
68
+ badgeSeverity: {
69
+ type: String as () => BadgeSeverity,
70
+ default: 'secondary',
71
+ validator: (v: string) => ['secondary', 'info', 'success', 'warn', 'danger', 'contrast'].includes(v)
72
+ },
73
+
74
+ /**
75
+ * Show/hide the tab
76
+ */
77
+ visible: {
78
+ type: Boolean,
79
+ default: true
80
+ },
81
+
82
+ /**
83
+ * Disable tab interaction
84
+ */
85
+ disabled: {
86
+ type: Boolean,
87
+ default: false
88
+ }
89
+ })
90
+
91
+ /**
92
+ * Normalize icon class
93
+ */
94
+ function getIconClass(): string | null {
95
+ if (!props.icon) return null
96
+ // Already has 'pi' prefix
97
+ if (props.icon.startsWith('pi')) {
98
+ return props.icon.includes(' ') ? props.icon : `pi ${props.icon}`
99
+ }
100
+ // Just icon name
101
+ return `pi pi-${props.icon}`
102
+ }
103
+
104
+ /**
105
+ * Get count value to display
106
+ */
107
+ function getCountValue(): number | null {
108
+ if (props.count !== null && props.count > 0) return props.count
109
+ return null
110
+ }
111
+
112
+ /**
113
+ * Get badge value to display (separate from count)
114
+ */
115
+ function getBadgeValue(): string | number | null {
116
+ if (props.badge !== null) return props.badge
117
+ // If no badge but count provided, show count as badge (legacy behavior)
118
+ if (props.count !== null && props.count > 0 && props.badge === null) return props.count
119
+ return null
120
+ }
121
+
122
+ /**
123
+ * Check if we should show count and badge separately
124
+ */
125
+ function showBothCountAndBadge(): boolean {
126
+ return props.count !== null && props.count > 0 && props.badge !== null
127
+ }
128
+ </script>
129
+
130
+ <template>
131
+ <Tab
132
+ v-if="visible"
133
+ :value="value"
134
+ :disabled="disabled"
135
+ >
136
+ <i v-if="icon" :class="getIconClass()" class="tab-icon" ></i>
137
+ <span class="tab-label">{{ label }}</span>
138
+ <!-- When both count and badge are provided, show count first then badge indicator -->
139
+ <template v-if="showBothCountAndBadge()">
140
+ <Tag
141
+ :value="String(getCountValue())"
142
+ severity="secondary"
143
+ class="tab-badge"
144
+ />
145
+ <span class="tab-alert-dot" :class="`alert-${badgeSeverity}`"></span>
146
+ </template>
147
+ <!-- Otherwise show single badge (legacy behavior) -->
148
+ <Tag
149
+ v-else-if="getBadgeValue() !== null"
150
+ :value="String(getBadgeValue())"
151
+ :severity="badgeSeverity"
152
+ class="tab-badge"
153
+ />
154
+ </Tab>
155
+ </template>
156
+
157
+ <style scoped>
158
+ /*
159
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
160
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
161
+ */
162
+ .tab-icon {
163
+ margin-right: 0.5rem;
164
+ }
165
+
166
+ .tab-label {
167
+ /* Ensure consistent text rendering */
168
+ }
169
+
170
+ .tab-badge {
171
+ margin-left: 0.5rem;
172
+ font-size: 0.7rem;
173
+ padding: 0.15rem 0.4rem;
174
+ min-width: 1.25rem;
175
+ text-align: center;
176
+ }
177
+
178
+ .tab-alert-dot {
179
+ display: inline-block;
180
+ width: 8px !important;
181
+ height: 8px !important;
182
+ min-width: 8px;
183
+ max-width: 8px;
184
+ border-radius: 50%;
185
+ margin-left: 0.35rem;
186
+ animation: pulse 1.5s infinite;
187
+ }
188
+
189
+ .alert-warn {
190
+ background-color: var(--p-orange-500);
191
+ }
192
+
193
+ .alert-danger {
194
+ background-color: var(--p-red-500);
195
+ }
196
+
197
+ .alert-info {
198
+ background-color: var(--p-blue-500);
199
+ }
200
+
201
+ .alert-success {
202
+ background-color: var(--p-green-500);
203
+ }
204
+
205
+ .alert-secondary {
206
+ background-color: var(--p-gray-500);
207
+ }
208
+
209
+ .alert-contrast {
210
+ background-color: var(--p-primary-500);
211
+ }
212
+
213
+ @keyframes pulse {
214
+ 0%, 100% { opacity: 1; }
215
+ 50% { opacity: 0.5; }
216
+ }
217
+ </style>