@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,98 @@
1
+ /**
2
+ * useAuth - Access authentication state (component-only)
3
+ *
4
+ * Provides access to authAdapter set via createQdadm bootstrap.
5
+ * Returns neutral values if auth is disabled.
6
+ *
7
+ * IMPORTANT: Must be called within component setup() function.
8
+ * For route guards or services, use authAdapter directly.
9
+ *
10
+ * Reactivity:
11
+ * - Kernel.invalidateApp() remounts entire app on auth changes
12
+ * - Composable is re-initialized with fresh state
13
+ * - No signal listeners needed
14
+ *
15
+ * Note: Permission checking (canRead/canWrite) is handled by EntityManager,
16
+ * not by useAuth. This keeps auth simple and delegates permission logic
17
+ * to where it belongs (the entity layer).
18
+ *
19
+ * Usage:
20
+ * const { isAuthenticated, user, logout } = useAuth()
21
+ */
22
+
23
+ import { inject, computed, ref, getCurrentInstance, type ComputedRef, type Ref } from 'vue'
24
+
25
+ /**
26
+ * Features configuration interface
27
+ */
28
+ interface FeaturesConfig {
29
+ auth?: boolean
30
+ [key: string]: unknown
31
+ }
32
+
33
+ /**
34
+ * Auth adapter interface
35
+ */
36
+ interface AuthAdapter {
37
+ login: (credentials?: unknown) => Promise<{ token: string | null; user: unknown }>
38
+ logout: () => void
39
+ getCurrentUser: () => Promise<unknown>
40
+ isAuthenticated: () => boolean
41
+ getUser?: () => unknown
42
+ user?: unknown
43
+ }
44
+
45
+ /**
46
+ * Return type for useAuth
47
+ */
48
+ export interface UseAuthReturn {
49
+ login: (credentials?: unknown) => Promise<{ token: string | null; user: unknown }>
50
+ logout: () => void
51
+ getCurrentUser: () => Promise<unknown>
52
+ isAuthenticated: ComputedRef<boolean>
53
+ user: ComputedRef<unknown> | Ref<null>
54
+ authEnabled: boolean
55
+ }
56
+
57
+ export function useAuth(): UseAuthReturn {
58
+ // Strict context check - must be called in component setup()
59
+ const instance = getCurrentInstance()
60
+ if (!instance) {
61
+ throw new Error('[qdadm] useAuth() must be called within component setup()')
62
+ }
63
+
64
+ const auth = inject<AuthAdapter | null>('authAdapter')
65
+ const features = inject<FeaturesConfig | null>('qdadmFeatures')
66
+
67
+ // If auth disabled or not provided, return neutral values
68
+ if (!features?.auth || !auth) {
69
+ return {
70
+ login: () => Promise.resolve({ token: null, user: null }),
71
+ logout: () => {},
72
+ getCurrentUser: () => Promise.resolve(null),
73
+ isAuthenticated: computed(() => true), // Always "authenticated"
74
+ user: ref(null),
75
+ authEnabled: false,
76
+ }
77
+ }
78
+
79
+ // Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
80
+ // on auth changes, so composable is re-initialized with fresh state
81
+
82
+ // User state - reads current value from authAdapter
83
+ const user = computed(() => {
84
+ if (typeof auth.getUser === 'function') {
85
+ return auth.getUser()
86
+ }
87
+ return auth.user || null
88
+ })
89
+
90
+ return {
91
+ login: auth.login,
92
+ logout: auth.logout,
93
+ getCurrentUser: auth.getCurrentUser,
94
+ isAuthenticated: computed(() => auth.isAuthenticated()),
95
+ user,
96
+ authEnabled: true,
97
+ }
98
+ }
@@ -0,0 +1,319 @@
1
+ /**
2
+ * useBareForm - Base form composable providing common form functionality
3
+ *
4
+ * Usage:
5
+ * const {
6
+ * // State
7
+ * loading, saving, dirty, isEdit, entityId,
8
+ * // Dirty tracking
9
+ * isFieldDirty, takeSnapshot, checkDirty,
10
+ * // Helpers
11
+ * router, route, toast, cancel
12
+ * } = useBareForm({
13
+ * getState: () => ({ form: form.value }),
14
+ * routePrefix: 'agents', // for cancel() navigation
15
+ * entityName: 'Agent', // for auto-title (optional, or use 'entity' for auto-lookup)
16
+ * labelField: 'name', // field to use as entity label (optional)
17
+ * entity: 'agents', // EntityManager name for auto metadata (optional)
18
+ * guard: true, // enable unsaved changes modal
19
+ * onGuardSave: () => save() // optional save callback for guard modal
20
+ * })
21
+ *
22
+ * This composable provides:
23
+ * - Dirty state tracking (form-level and field-level)
24
+ * - isFieldDirty for child FormField components via inject
25
+ * - Unsaved changes guard modal
26
+ * - Common state refs (loading, saving)
27
+ * - Common computed (isEdit, entityId)
28
+ * - Navigation helpers (cancel)
29
+ * - Access to router, route, toast
30
+ * - Auto page title via provide (for PageHeader)
31
+ */
32
+ import { ref, computed, provide, inject, onUnmounted, type Ref, type ComputedRef } from 'vue'
33
+ import { useRoute, useRouter, type Router, type RouteLocationNormalizedLoaded } from 'vue-router'
34
+ import { useDirtyState } from './useDirtyState'
35
+ import { useUnsavedChangesGuard, type GuardDialogState } from './useUnsavedChangesGuard'
36
+ import { useBreadcrumb, type BreadcrumbDisplayItem } from './useBreadcrumb'
37
+ import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
38
+
39
+ /**
40
+ * Entity manager interface (minimal for this composable)
41
+ */
42
+ interface EntityManager {
43
+ label?: string
44
+ labelField?: string
45
+ getEntityLabel: (data: unknown) => string
46
+ }
47
+
48
+ /**
49
+ * Orchestrator interface
50
+ */
51
+ interface Orchestrator {
52
+ get: (entityName: string) => EntityManager
53
+ toast: {
54
+ success: (summary: string, detail?: string, emitter?: unknown) => void
55
+ error: (summary: string, detail?: string, emitter?: unknown) => void
56
+ warn: (summary: string, detail?: string, emitter?: unknown) => void
57
+ info: (summary: string, detail?: string, emitter?: unknown) => void
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Toast helper interface
63
+ */
64
+ interface ToastHelper {
65
+ add: (options: {
66
+ severity: 'success' | 'error' | 'warn' | 'info'
67
+ summary: string
68
+ detail?: string
69
+ emitter?: unknown
70
+ }) => void
71
+ }
72
+
73
+ /**
74
+ * Page title parts for PageHeader
75
+ */
76
+ interface PageTitleParts {
77
+ action: string
78
+ entityName: string | null
79
+ entityLabel: string | null
80
+ }
81
+
82
+ /**
83
+ * Options for useBareForm
84
+ */
85
+ export interface UseBareFormOptions {
86
+ /** Function returning current form state for comparison */
87
+ getState: () => Record<string, unknown>
88
+ /** Route name for cancel navigation (default: '') */
89
+ routePrefix?: string
90
+ /** Display name for entity (default: from manager or derived from routePrefix) */
91
+ entityName?: string | null
92
+ /** Field name or function to extract entity label (default: from manager or 'name') */
93
+ labelField?: string | ((data: unknown) => string) | null
94
+ /** EntityManager name (string) for auto metadata, OR entity data (Ref/Object) for breadcrumb */
95
+ entity?: string | Ref<unknown> | Record<string, unknown> | null
96
+ /** Enable unsaved changes guard (default: true) */
97
+ guard?: boolean
98
+ /** Callback for save button in guard modal */
99
+ onGuardSave?: (() => Promise<void>) | null
100
+ /** Custom function to extract entity ID from route (optional) */
101
+ getId?: (() => string | number | null) | null
102
+ /** Callback (entity) => string for custom breadcrumb label (optional) */
103
+ breadcrumbLabel?: ((entity: unknown) => string) | null
104
+ }
105
+
106
+ /**
107
+ * Return type for useBareForm
108
+ */
109
+ export interface UseBareFormReturn {
110
+ // Dependencies (for custom logic in form)
111
+ router: Router
112
+ route: RouteLocationNormalizedLoaded
113
+ toast: ToastHelper
114
+
115
+ // Manager (if resolved from entity string)
116
+ manager: EntityManager | null
117
+
118
+ // State
119
+ loading: Ref<boolean>
120
+ saving: Ref<boolean>
121
+ dirty: Ref<boolean>
122
+ dirtyFields: Ref<string[]>
123
+ isEdit: ComputedRef<boolean>
124
+ entityId: ComputedRef<string | number | null>
125
+
126
+ // Dirty tracking
127
+ isFieldDirty: (fieldPath: string) => boolean
128
+ takeSnapshot: () => void
129
+ checkDirty: () => void
130
+ reset: () => void
131
+
132
+ // Helpers
133
+ cancel: () => void
134
+
135
+ // Breadcrumb
136
+ breadcrumb: ComputedRef<BreadcrumbDisplayItem[]>
137
+
138
+ // Guard dialog (for UnsavedChangesDialog component)
139
+ guardDialog: GuardDialogState | null
140
+
141
+ // Title helpers
142
+ pageTitleParts: ComputedRef<PageTitleParts>
143
+ }
144
+
145
+ /**
146
+ * Base form composable providing common form functionality
147
+ *
148
+ * @param options - Configuration options
149
+ * @returns Form utilities and state
150
+ */
151
+ export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
152
+ const {
153
+ getState,
154
+ routePrefix = '',
155
+ entityName = null,
156
+ labelField = null,
157
+ guard = true,
158
+ onGuardSave = null,
159
+ getId = null,
160
+ entity = null,
161
+ breadcrumbLabel = null,
162
+ } = options
163
+
164
+ if (!getState || typeof getState !== 'function') {
165
+ throw new Error('useBareForm requires a getState function')
166
+ }
167
+
168
+ // Try to get EntityManager metadata if entity is a string
169
+ let manager: EntityManager | null = null
170
+ if (typeof entity === 'string') {
171
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
172
+ if (orchestrator) {
173
+ try {
174
+ manager = orchestrator.get(entity)
175
+ } catch {
176
+ // Manager not found, continue without it
177
+ }
178
+ }
179
+ }
180
+
181
+ // Router, route, toast - common dependencies
182
+ const router = useRouter()
183
+ const route = useRoute()
184
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
185
+
186
+ // Toast helper - wraps orchestrator.toast for legacy compatibility
187
+ const toast: ToastHelper = {
188
+ add({ severity, summary, detail, emitter }) {
189
+ orchestrator?.toast[severity]?.(summary, detail, emitter)
190
+ },
191
+ }
192
+
193
+ // Common state
194
+ const loading = ref(false)
195
+ const saving = ref(false)
196
+
197
+ // Common computed
198
+ const entityId = computed(() => {
199
+ if (getId) return getId()
200
+ return (route.params.id as string | number) || (route.params.key as string | number) || null
201
+ })
202
+
203
+ const isEdit = computed(() => !!entityId.value)
204
+
205
+ // Dirty state tracking
206
+ const { dirty, dirtyFields, isFieldDirty, takeSnapshot, checkDirty, reset } =
207
+ useDirtyState(getState)
208
+
209
+ // Provide isFieldDirty and dirtyFields for child components (FormField)
210
+ provide('isFieldDirty', isFieldDirty)
211
+ provide('dirtyFields', dirtyFields)
212
+
213
+ // Resolve entityName: explicit > manager > derived from routePrefix
214
+ const derivedEntityName = routePrefix
215
+ ? routePrefix.charAt(0).toUpperCase() + routePrefix.slice(1).replace(/s$/, '')
216
+ : null
217
+ const effectiveEntityName = entityName || manager?.label || derivedEntityName
218
+
219
+ // Resolve labelField: explicit > manager > default 'name'
220
+ const effectiveLabelField = labelField || manager?.labelField || 'name'
221
+
222
+ // Provide entity context for child components (e.g., SeverityTag auto-discovery)
223
+ if (routePrefix) {
224
+ const entityNameForProvider = routePrefix.endsWith('s') ? routePrefix : routePrefix + 's'
225
+ provide('mainEntity', entityNameForProvider)
226
+ } else if (typeof entity === 'string') {
227
+ provide('mainEntity', entity)
228
+ }
229
+
230
+ // Auto page title parts for PageHeader
231
+ const getEntityLabel = (): string | null => {
232
+ const state = getState()
233
+ const formData = (state.form as Record<string, unknown>) || state
234
+ if (!formData) return null
235
+ // Use manager.getEntityLabel if available, otherwise use effectiveLabelField
236
+ if (manager) {
237
+ return manager.getEntityLabel(formData)
238
+ }
239
+ if (typeof effectiveLabelField === 'function') {
240
+ return effectiveLabelField(formData)
241
+ }
242
+ return (formData[effectiveLabelField as string] as string) || null
243
+ }
244
+
245
+ const pageTitleParts = computed<PageTitleParts>(() => ({
246
+ action: isEdit.value ? 'Edit' : 'Create',
247
+ entityName: effectiveEntityName,
248
+ entityLabel: isEdit.value ? getEntityLabel() : null,
249
+ }))
250
+
251
+ // Provide title parts for automatic PageHeader consumption
252
+ if (effectiveEntityName) {
253
+ provide('qdadmPageTitleParts', pageTitleParts)
254
+ }
255
+
256
+ // Breadcrumb (auto-generated from route path, with optional entity for dynamic labels)
257
+ // Only pass entity to breadcrumb if it's actual entity data (not a string manager name)
258
+ const breadcrumbEntity = typeof entity === 'string' ? null : entity
259
+ const { breadcrumbItems } = useBreadcrumb({
260
+ entity: breadcrumbEntity as unknown | Ref<unknown> | undefined,
261
+ getEntityLabel: breadcrumbLabel || undefined,
262
+ })
263
+
264
+ // Unsaved changes guard
265
+ let guardDialog: GuardDialogState | null = null
266
+ if (guard) {
267
+ const guardOptions = onGuardSave ? { onSave: onGuardSave } : {}
268
+ const guardResult = useUnsavedChangesGuard(dirty, guardOptions)
269
+ guardDialog = guardResult.guardDialog
270
+ // Register guardDialog in shared store so AppLayout can render it
271
+ registerGuardDialog(guardDialog)
272
+ onUnmounted(() => unregisterGuardDialog(guardDialog!))
273
+ }
274
+
275
+ // Navigation helper
276
+ function cancel(): void {
277
+ if (routePrefix) {
278
+ router.push({ name: routePrefix })
279
+ } else {
280
+ router.back()
281
+ }
282
+ }
283
+
284
+ return {
285
+ // Dependencies (for custom logic in form)
286
+ router,
287
+ route,
288
+ toast,
289
+
290
+ // Manager (if resolved from entity string)
291
+ manager,
292
+
293
+ // State
294
+ loading,
295
+ saving,
296
+ dirty,
297
+ dirtyFields,
298
+ isEdit,
299
+ entityId,
300
+
301
+ // Dirty tracking
302
+ isFieldDirty,
303
+ takeSnapshot,
304
+ checkDirty,
305
+ reset,
306
+
307
+ // Helpers
308
+ cancel,
309
+
310
+ // Breadcrumb
311
+ breadcrumb: breadcrumbItems,
312
+
313
+ // Guard dialog (for UnsavedChangesDialog component)
314
+ guardDialog,
315
+
316
+ // Title helpers
317
+ pageTitleParts,
318
+ }
319
+ }
@@ -0,0 +1,252 @@
1
+ import { computed, inject, type ComputedRef, type Ref } from 'vue'
2
+ import { useRouter } from 'vue-router'
3
+ import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
4
+ import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
5
+
6
+ /**
7
+ * Breadcrumb display item
8
+ */
9
+ export interface BreadcrumbDisplayItem {
10
+ label: string
11
+ to: { name: string; params?: Record<string, unknown> } | null
12
+ icon: string | null
13
+ }
14
+
15
+ /**
16
+ * Options for useBreadcrumb
17
+ */
18
+ export interface UseBreadcrumbOptions {
19
+ /** Current entity data for dynamic labels */
20
+ entity?: unknown | Ref<unknown>
21
+ /** Custom label resolver for entities */
22
+ getEntityLabel?: (entity: unknown) => string
23
+ /** Custom label mappings */
24
+ labelMap?: Record<string, string>
25
+ /** Custom icon mappings */
26
+ iconMap?: Record<string, string>
27
+ }
28
+
29
+ /**
30
+ * Return type for useBreadcrumb
31
+ */
32
+ export interface UseBreadcrumbReturn {
33
+ breadcrumbItems: ComputedRef<BreadcrumbDisplayItem[]>
34
+ semanticBreadcrumb: ComputedRef<SemanticBreadcrumbItem[]>
35
+ setBreadcrumb: (items: BreadcrumbDisplayItem[]) => BreadcrumbDisplayItem[]
36
+ }
37
+
38
+ /**
39
+ * useBreadcrumb - Generate display-ready breadcrumb from semantic breadcrumb
40
+ *
41
+ * Uses useSemanticBreadcrumb for the semantic model, then applies adapters
42
+ * to resolve labels, paths, and icons for display.
43
+ *
44
+ * @param options - Options
45
+ *
46
+ * @example
47
+ * const { breadcrumbItems, semanticBreadcrumb } = useBreadcrumb()
48
+ *
49
+ * // With entity data for dynamic labels
50
+ * const { breadcrumbItems } = useBreadcrumb({ entity: bookData })
51
+ */
52
+ export function useBreadcrumb(options: UseBreadcrumbOptions = {}): UseBreadcrumbReturn {
53
+ const router = useRouter()
54
+ const homeRouteName = inject<string | null>('qdadmHomeRoute', null)
55
+
56
+ // Get semantic breadcrumb
57
+ const { breadcrumb: semanticBreadcrumb } = useSemanticBreadcrumb()
58
+
59
+ // Get hydrator for parent entity labels
60
+ const hydrator = useStackHydrator()
61
+
62
+ // Default label mapping
63
+ const defaultLabelMap: Record<string, string> = {
64
+ home: 'Home',
65
+ dashboard: 'Dashboard',
66
+ create: 'Create',
67
+ edit: 'Edit',
68
+ show: 'View',
69
+ delete: 'Delete',
70
+ }
71
+
72
+ // Default icon mapping for entities
73
+ const defaultIconMap: Record<string, string> = {
74
+ users: 'pi pi-users',
75
+ roles: 'pi pi-shield',
76
+ books: 'pi pi-book',
77
+ genres: 'pi pi-tags',
78
+ loans: 'pi pi-exchange',
79
+ settings: 'pi pi-cog',
80
+ }
81
+
82
+ // Merge with custom mappings
83
+ const labelMap = { ...defaultLabelMap, ...options.labelMap }
84
+ const iconMap = { ...defaultIconMap, ...options.iconMap }
85
+
86
+ /**
87
+ * Capitalize first letter
88
+ */
89
+ function capitalize(str: string): string {
90
+ if (!str) return ''
91
+ return str.charAt(0).toUpperCase() + str.slice(1)
92
+ }
93
+
94
+ /**
95
+ * Get route name for entity
96
+ */
97
+ function getEntityRouteName(entity: string, kind: string): string {
98
+ // Convention: entity-list -> 'book', entity-edit -> 'book-edit'
99
+ const singular = entity.endsWith('s') ? entity.slice(0, -1) : entity
100
+ if (kind === 'entity-list') return singular
101
+ if (kind === 'entity-show') return `${singular}-show`
102
+ if (kind === 'entity-edit') return `${singular}-edit`
103
+ if (kind === 'entity-create') return `${singular}-create`
104
+ return singular
105
+ }
106
+
107
+ /**
108
+ * Check if a route exists
109
+ */
110
+ function routeExists(name: string): boolean {
111
+ return router.getRoutes().some((r) => r.name === name)
112
+ }
113
+
114
+ /**
115
+ * Resolve semantic item to display item
116
+ * @param item - Semantic breadcrumb item
117
+ * @param index - Index in the breadcrumb array
118
+ * @param entity - Current entity data (for labels)
119
+ * @param isLast - Whether this is the last item (no link)
120
+ */
121
+ function resolveItem(
122
+ item: SemanticBreadcrumbItem,
123
+ index: number,
124
+ entity: unknown,
125
+ isLast: boolean
126
+ ): BreadcrumbDisplayItem {
127
+ const displayItem: BreadcrumbDisplayItem = {
128
+ label: '',
129
+ to: null,
130
+ icon: null,
131
+ }
132
+
133
+ if (item.kind === 'route') {
134
+ // Generic route
135
+ const routeName = item.route!
136
+ displayItem.label = item.label || labelMap[routeName] || capitalize(routeName)
137
+ displayItem.to = isLast ? null : routeExists(routeName) ? { name: routeName } : null
138
+ if (routeName === 'home') {
139
+ displayItem.icon = 'pi pi-home'
140
+ }
141
+ } else if (item.kind.startsWith('entity-')) {
142
+ // Entity-related item
143
+ const entityName = item.entity!
144
+
145
+ if (item.kind === 'entity-list') {
146
+ // Entity list - use plural label
147
+ displayItem.label = labelMap[entityName] || capitalize(entityName)
148
+ displayItem.icon = index === 1 ? iconMap[entityName] || null : null
149
+ const routeName = getEntityRouteName(entityName, item.kind)
150
+ displayItem.to = isLast ? null : routeExists(routeName) ? { name: routeName } : null
151
+ } else if (item.id) {
152
+ // Entity instance (show/edit/delete)
153
+ // Try to get label from hydrator first (works for all levels)
154
+ const hydratedLevel = hydrator.getLevelByEntity(entityName)
155
+ if (hydratedLevel?.label) {
156
+ displayItem.label = hydratedLevel.label
157
+ } else if (entity && options.getEntityLabel) {
158
+ displayItem.label = options.getEntityLabel(entity)
159
+ } else if (entity && typeof entity === 'object' && (entity as Record<string, unknown>).name) {
160
+ displayItem.label = String((entity as Record<string, unknown>).name)
161
+ } else if (
162
+ entity &&
163
+ typeof entity === 'object' &&
164
+ (entity as Record<string, unknown>).title
165
+ ) {
166
+ displayItem.label = String((entity as Record<string, unknown>).title)
167
+ } else {
168
+ displayItem.label = `#${item.id}`
169
+ }
170
+
171
+ // Last item = no link. Otherwise link to show page if exists
172
+ if (!isLast && (item.kind === 'entity-edit' || item.kind === 'entity-delete')) {
173
+ const showRouteName = getEntityRouteName(entityName, 'entity-show')
174
+ if (routeExists(showRouteName)) {
175
+ displayItem.to = { name: showRouteName, params: { id: item.id } }
176
+ }
177
+ }
178
+ } else if (item.kind === 'entity-create') {
179
+ // Create page - label as action
180
+ displayItem.label = labelMap.create || 'Create'
181
+ }
182
+ }
183
+
184
+ return displayItem
185
+ }
186
+
187
+ /**
188
+ * Get home breadcrumb item
189
+ */
190
+ function getHomeItem(): BreadcrumbDisplayItem | null {
191
+ if (!homeRouteName || !routeExists(homeRouteName)) return null
192
+ return {
193
+ label: labelMap.home || 'Home',
194
+ to: { name: homeRouteName },
195
+ icon: 'pi pi-home',
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Computed display-ready breadcrumb items
201
+ */
202
+ const breadcrumbItems = computed(() => {
203
+ const entity =
204
+ (options.entity as Ref<unknown>)?.value !== undefined
205
+ ? (options.entity as Ref<unknown>).value
206
+ : options.entity
207
+ const items: BreadcrumbDisplayItem[] = []
208
+
209
+ // Access hydrator levels deeply to create Vue dependencies
210
+ // This ensures breadcrumb recomputes when hydration completes
211
+ // We need to touch each level's label to track changes
212
+ const _hydratorLevels = hydrator.levels.value
213
+ // Touch labels to track changes (result intentionally unused for reactivity)
214
+ _hydratorLevels.map((l: HydratedLevel) => l.label).join()
215
+
216
+ // Add home first (display concern, not in semantic breadcrumb)
217
+ const homeItem = getHomeItem()
218
+ if (homeItem) {
219
+ items.push(homeItem)
220
+ }
221
+
222
+ // Resolve semantic items to display items
223
+ const semanticItems = semanticBreadcrumb.value
224
+ for (let i = 0; i < semanticItems.length; i++) {
225
+ const semanticItem = semanticItems[i]
226
+ if (!semanticItem) continue
227
+ const isLast = i === semanticItems.length - 1
228
+ const displayItem = resolveItem(semanticItem, i, entity, isLast)
229
+ items.push(displayItem)
230
+ }
231
+
232
+ return items
233
+ })
234
+
235
+ /**
236
+ * Manual override - set custom breadcrumb items
237
+ */
238
+ function setBreadcrumb(items: BreadcrumbDisplayItem[]): BreadcrumbDisplayItem[] {
239
+ const homeItem: BreadcrumbDisplayItem = {
240
+ label: labelMap.home || 'Home',
241
+ to: { name: homeRouteName! },
242
+ icon: 'pi pi-home',
243
+ }
244
+ return homeRouteName && routeExists(homeRouteName) ? [homeItem, ...items] : items
245
+ }
246
+
247
+ return {
248
+ breadcrumbItems,
249
+ semanticBreadcrumb,
250
+ setBreadcrumb,
251
+ }
252
+ }