@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,323 @@
1
+ /**
2
+ * useOptionsLookup - Generic composable for autocomplete/dropdown options
3
+ *
4
+ * Fetches options from entity managers or API endpoints for use in any form field.
5
+ * Two modes:
6
+ * - **Pure mode** (default for string/number arrays): returns raw values directly.
7
+ * Use with MultiSelect :options or AutoComplete :suggestions on string[] fields.
8
+ * - **Mapped mode** (objects with label/value): returns { label, value } items.
9
+ * Use when source items are objects and you need label ≠ value.
10
+ *
11
+ * Mapped mode display strategies (displayMode):
12
+ * - **bracket** (default): suggestions show "Label [value]", decode extracts from brackets.
13
+ * - **hidden**: suggestions show only "Label", value is resolved via internal lookup.
14
+ * If the user types a label not in the list, decode returns the typed string as-is.
15
+ *
16
+ * Usage:
17
+ * ```ts
18
+ * // Pure strings — suggestions are string[]
19
+ * const tags = useOptionsLookup({ endpoint: '/api/admin/tags' })
20
+ *
21
+ * // Mapped mode with bracket display (default)
22
+ * const pools = useOptionsLookup({ entity: 'botPools', label: 'name', value: 'id' })
23
+ * // suggestions = ["Pool Alpha [pool-1]", "Pool Beta [pool-2]"]
24
+ * // pools.decode("Pool Alpha [pool-1]") → "pool-1"
25
+ *
26
+ * // Mapped mode with hidden display — user sees labels only
27
+ * const pools = useOptionsLookup({
28
+ * entity: 'botPools', label: 'name', value: 'id',
29
+ * displayMode: 'hidden',
30
+ * })
31
+ * // suggestions = ["Pool Alpha", "Pool Beta"]
32
+ * // pools.decode("Pool Alpha") → "pool-1"
33
+ * // pools.resolve("pool-1") → "Pool Alpha"
34
+ *
35
+ * // Custom encode/decode (overrides displayMode)
36
+ * const custom = useOptionsLookup({
37
+ * entity: 'users', label: 'username', value: 'id',
38
+ * encode: (item) => `${item.label} (#${item.value})`,
39
+ * decode: (str) => { const m = str.match(/#(.+)\)$/); return m?.[1] ?? str },
40
+ * })
41
+ * ```
42
+ */
43
+ import { ref, onMounted, type Ref } from 'vue'
44
+ import { useOrchestrator } from '../orchestrator/useOrchestrator.js'
45
+
46
+ export interface OptionsLookupItem {
47
+ label: string
48
+ value: unknown
49
+ }
50
+
51
+ /** Default encode: "Label [value]" */
52
+ function defaultEncode(item: OptionsLookupItem): string {
53
+ return `${item.label} [${item.value}]`
54
+ }
55
+
56
+ /** Default decode: extract value from trailing "[value]" */
57
+ function defaultDecode(encoded: string): unknown {
58
+ const match = encoded.match(/\[([^\]]+)\]\s*$/)
59
+ return match ? match[1] : encoded
60
+ }
61
+
62
+ export type OptionsDisplayMode = 'bracket' | 'hidden'
63
+
64
+ export interface UseOptionsLookupConfig {
65
+ /** Fetch options from an entity manager (via orchestrator) */
66
+ entity?: string
67
+ /** Extract distinct values from a specific field (entity mode only) */
68
+ field?: string
69
+ /** Fetch options from a raw API endpoint */
70
+ endpoint?: string
71
+ /** When using endpoint: pick a sub-key from the response data */
72
+ pick?: string
73
+ /** Extra headers for endpoint fetch (e.g. Authorization) */
74
+ headers?: Record<string, string> | (() => Record<string, string>)
75
+ /** Static options (no fetch needed) */
76
+ static?: unknown[]
77
+ /** Field to use as display label (default: 'name'). Only for mapped mode. */
78
+ label?: string
79
+ /** Field to use as value (default: 'id'). Only for mapped mode. */
80
+ value?: string
81
+ /**
82
+ * How to display mapped values in autocomplete (default: 'bracket').
83
+ * - 'bracket': "Label [value]" — value visible, decode parses brackets
84
+ * - 'hidden': "Label" — value hidden, decode uses internal label→value lookup
85
+ * Ignored when custom encode/decode are provided.
86
+ */
87
+ displayMode?: OptionsDisplayMode
88
+ /** Custom encode function: item → display string (overrides displayMode) */
89
+ encode?: (item: OptionsLookupItem) => string
90
+ /** Custom decode function: display string → raw value (overrides displayMode) */
91
+ decode?: (encoded: string) => unknown
92
+ /** Post-process mapped options */
93
+ transform?: (items: OptionsLookupItem[]) => OptionsLookupItem[]
94
+ /** Auto-load on mount (default: true) */
95
+ autoLoad?: boolean
96
+ /** Threshold: use autocomplete UI if options exceed this count */
97
+ autocompleteThreshold?: number
98
+ }
99
+
100
+ export interface UseOptionsLookupReturn {
101
+ /** Mapped options ({ label, value }[]) for complex dropdowns */
102
+ options: Ref<OptionsLookupItem[]>
103
+ /** Pure values (string[] or number[]) for simple MultiSelect/AutoComplete */
104
+ values: Ref<unknown[]>
105
+ /** Raw items as fetched (before any mapping) */
106
+ raw: Ref<unknown[]>
107
+ /** Loading state */
108
+ loading: Ref<boolean>
109
+ /** Error message if fetch failed */
110
+ error: Ref<string | null>
111
+ /** Filtered suggestions — always string[] (pure values or encoded strings) */
112
+ suggestions: Ref<string[]>
113
+ /** Whether autocomplete is recommended (options exceed threshold) */
114
+ useAutocomplete: Ref<boolean>
115
+ /** Whether data is pure primitives (strings/numbers) */
116
+ isPure: Ref<boolean>
117
+ /** Filter options by query string (for AutoComplete @complete) */
118
+ search: (query: string) => void
119
+ /** Manually (re)load options */
120
+ load: () => Promise<void>
121
+ /** Decode an encoded display string back to raw value. Identity in pure mode. */
122
+ decode: (encoded: string) => unknown
123
+ /** Encode an item { label, value } to display string. */
124
+ encode: (item: OptionsLookupItem) => string
125
+ /** Resolve a raw value to its encoded display string. Returns String(val) if not found. */
126
+ resolve: (rawValue: unknown) => string
127
+ }
128
+
129
+ export function useOptionsLookup(config: UseOptionsLookupConfig): UseOptionsLookupReturn {
130
+ const options = ref<OptionsLookupItem[]>([])
131
+ const values = ref<unknown[]>([])
132
+ const raw = ref<unknown[]>([])
133
+ const loading = ref(false)
134
+ const error = ref<string | null>(null)
135
+ const suggestions = ref<string[]>([])
136
+ const useAutocomplete = ref(false)
137
+ const isPure = ref(false)
138
+
139
+ const labelField = config.label ?? 'name'
140
+ const valueField = config.value ?? 'id'
141
+ const threshold = config.autocompleteThreshold ?? 50
142
+ const displayMode = config.displayMode ?? 'bracket'
143
+
144
+ // Label→value map for hidden mode decode (rebuilt on processItems)
145
+ let labelToValue = new Map<string, unknown>()
146
+
147
+ // Resolve encode/decode based on displayMode (custom overrides all)
148
+ const encodeFn = config.encode ?? (
149
+ displayMode === 'hidden'
150
+ ? (item: OptionsLookupItem) => item.label
151
+ : defaultEncode
152
+ )
153
+ const decodeFn = config.decode ?? (
154
+ displayMode === 'hidden'
155
+ ? (encoded: string) => {
156
+ // Exact match first, then case-insensitive
157
+ if (labelToValue.has(encoded)) return labelToValue.get(encoded)
158
+ const lower = encoded.toLowerCase()
159
+ for (const [label, value] of labelToValue) {
160
+ if (label.toLowerCase() === lower) return value
161
+ }
162
+ return encoded // Not found → return as-is (manual input)
163
+ }
164
+ : defaultDecode
165
+ )
166
+
167
+ // Resolve orchestrator lazily (only if entity mode)
168
+ let getManager: ((name: string) => { list: (params?: Record<string, unknown>) => Promise<{ items: unknown[] }> }) | null = null
169
+
170
+ if (config.entity) {
171
+ try {
172
+ const orc = useOrchestrator()
173
+ // Generic-vs-non-generic: getManager is parameterised by an
174
+ // EntityRecord type variable that the local alias intentionally
175
+ // erases. Bridge through `unknown` so strict TS doesn't reject
176
+ // the cross-shape assignment.
177
+ getManager = orc.getManager as unknown as typeof getManager
178
+ } catch {
179
+ // Orchestrator not available
180
+ }
181
+ }
182
+
183
+ // Encoded strings cache for mapped mode (rebuilt on processItems)
184
+ let encodedStrings: string[] = []
185
+
186
+ function processItems(items: unknown[]): void {
187
+ raw.value = items
188
+
189
+ // Detect pure mode: all items are primitives (string/number)
190
+ const allPrimitive = items.length > 0 && items.every(
191
+ (item) => typeof item === 'string' || typeof item === 'number',
192
+ )
193
+ isPure.value = allPrimitive
194
+
195
+ if (allPrimitive) {
196
+ // Pure mode — values are the items themselves
197
+ values.value = items
198
+ options.value = items.map((item) => ({ label: String(item), value: item }))
199
+ encodedStrings = items.map(String)
200
+ } else {
201
+ // Mapped mode — extract label/value from objects
202
+ options.value = items.map((item) => {
203
+ const obj = item as Record<string, unknown>
204
+ return {
205
+ label: String(obj[labelField] ?? obj.label ?? obj.name ?? obj[valueField] ?? item),
206
+ value: obj[valueField] ?? obj.value ?? obj.id ?? item,
207
+ }
208
+ })
209
+ values.value = options.value.map((opt) => opt.value)
210
+
211
+ if (config.transform) options.value = config.transform(options.value)
212
+
213
+ // Pre-compute encoded strings for suggestions
214
+ encodedStrings = options.value.map(encodeFn)
215
+
216
+ // Build label→value map for hidden mode decode
217
+ labelToValue = new Map(options.value.map((opt) => [encodeFn(opt), opt.value]))
218
+ }
219
+
220
+ useAutocomplete.value = items.length > threshold
221
+
222
+ // Pre-fill suggestions so dropdown button works on first click
223
+ suggestions.value = encodedStrings
224
+ }
225
+
226
+ async function load(): Promise<void> {
227
+ // Static mode — no fetch
228
+ if (config.static) {
229
+ processItems(config.static)
230
+ return
231
+ }
232
+
233
+ loading.value = true
234
+ error.value = null
235
+
236
+ try {
237
+ let items: unknown[] = []
238
+
239
+ if (config.entity && getManager) {
240
+ const manager = getManager(config.entity)
241
+ const result = await manager.list({ page_size: 10000 })
242
+ const rawItems = result.items ?? []
243
+
244
+ // If field is specified, extract distinct values from that field
245
+ if (config.field) {
246
+ const seen = new Set<unknown>()
247
+ for (const item of rawItems) {
248
+ const val = (item as Record<string, unknown>)[config.field]
249
+ if (val != null && !seen.has(val)) {
250
+ seen.add(val)
251
+ items.push(val)
252
+ }
253
+ }
254
+ } else {
255
+ items = rawItems
256
+ }
257
+ } else if (config.endpoint) {
258
+ const extraHeaders = typeof config.headers === 'function' ? config.headers() : config.headers
259
+ const response = await fetch(config.endpoint, {
260
+ credentials: 'include',
261
+ headers: { 'Accept': 'application/json', ...extraHeaders },
262
+ })
263
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
264
+ const json = await response.json()
265
+
266
+ let data = json.data ?? json
267
+
268
+ if (config.pick && typeof data === 'object' && !Array.isArray(data)) {
269
+ data = (data as Record<string, unknown>)[config.pick]
270
+ }
271
+
272
+ items = Array.isArray(data) ? data : []
273
+ }
274
+
275
+ processItems(items)
276
+ } catch (err) {
277
+ error.value = err instanceof Error ? err.message : String(err)
278
+ } finally {
279
+ loading.value = false
280
+ }
281
+ }
282
+
283
+ function search(query: string): void {
284
+ let result: string[]
285
+ if (!query) {
286
+ result = encodedStrings
287
+ } else {
288
+ const q = query.toLowerCase()
289
+ const filtered = encodedStrings.filter(
290
+ (s) => s.toLowerCase().includes(q),
291
+ )
292
+ // When query exactly matches a single option (e.g. dropdown click with
293
+ // current value), show all options so the user can browse alternatives
294
+ result = (filtered.length === 1 && filtered[0].toLowerCase() === q)
295
+ ? encodedStrings
296
+ : filtered
297
+ }
298
+ // Always assign a new array reference so PrimeVue detects the change
299
+ suggestions.value = [...result]
300
+ }
301
+
302
+ function decode(encoded: string): unknown {
303
+ if (isPure.value) return encoded
304
+ return decodeFn(encoded)
305
+ }
306
+
307
+ function encode(item: OptionsLookupItem): string {
308
+ return encodeFn(item)
309
+ }
310
+
311
+ function resolve(rawValue: unknown): string {
312
+ const opt = options.value.find((o) => o.value === rawValue || String(o.value) === String(rawValue))
313
+ if (!opt) return String(rawValue ?? '')
314
+ if (isPure.value) return opt.label
315
+ return encodeFn(opt)
316
+ }
317
+
318
+ if (config.autoLoad !== false) {
319
+ onMounted(() => load())
320
+ }
321
+
322
+ return { options, values, raw, loading, error, suggestions, useAutocomplete, isPure, search, load, decode, encode, resolve }
323
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * usePageTitle - Provide custom page title for PageHeader
3
+ *
4
+ * Use this composable in custom pages to set the title displayed in PageHeader.
5
+ * For standard CRUD pages, useForm handles this automatically.
6
+ *
7
+ * Usage:
8
+ * ```ts
9
+ * // Simple title
10
+ * const { setTitle } = usePageTitle('My Custom Page')
11
+ *
12
+ * // Decorated title (entityLabel shown prominently, action+entityName as badge)
13
+ * usePageTitle({
14
+ * action: 'View',
15
+ * entityName: 'Stats',
16
+ * entityLabel: 'Dashboard'
17
+ * })
18
+ *
19
+ * // Reactive updates
20
+ * const { setTitle } = usePageTitle('Initial')
21
+ * setTitle('Updated Title')
22
+ * ```
23
+ */
24
+ import { ref, provide, watchEffect, isRef, unref, type Ref } from 'vue'
25
+
26
+ /**
27
+ * Title parts for decorated title display
28
+ */
29
+ export interface TitleParts {
30
+ simple?: string
31
+ action?: string
32
+ entityName?: string
33
+ entityLabel?: string
34
+ }
35
+
36
+ /**
37
+ * Title input type - can be string or TitleParts object
38
+ */
39
+ export type TitleInput = string | TitleParts | null
40
+
41
+ /**
42
+ * Return type for usePageTitle
43
+ */
44
+ export interface UsePageTitleReturn {
45
+ titleParts: Ref<TitleParts | null>
46
+ setTitle: (newTitle: TitleInput | Ref<TitleInput>) => void
47
+ }
48
+
49
+ export function usePageTitle(
50
+ initialTitle: TitleInput | Ref<TitleInput> = null
51
+ ): UsePageTitleReturn {
52
+ const titleParts = ref<TitleParts | null>(null)
53
+
54
+ // Convert string to titleParts format (simple title)
55
+ function normalize(title: TitleInput): TitleParts | null {
56
+ if (!title) return null
57
+ if (typeof title === 'string') {
58
+ return { simple: title }
59
+ }
60
+ return title
61
+ }
62
+
63
+ // Set title (string or { action, entityName, entityLabel })
64
+ function setTitle(newTitle: TitleInput | Ref<TitleInput>): void {
65
+ titleParts.value = normalize(unref(newTitle))
66
+ }
67
+
68
+ // Initialize with provided value
69
+ if (initialTitle !== null) {
70
+ if (isRef(initialTitle)) {
71
+ // Reactive: watch for changes
72
+ watchEffect(() => {
73
+ setTitle(initialTitle.value)
74
+ })
75
+ } else {
76
+ setTitle(initialTitle)
77
+ }
78
+ }
79
+
80
+ // Provide to PageHeader via same key as useForm
81
+ provide('qdadmPageTitleParts', titleParts)
82
+
83
+ return { titleParts, setTitle }
84
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * useSSEBridge - Composable for SSE connection status
3
+ *
4
+ * Provides reactive connection status for SSEBridge.
5
+ * Components use this instead of managing their own EventSource.
6
+ *
7
+ * Usage:
8
+ * ```ts
9
+ * const { connected, reconnecting, error, onEvent } = useSSEBridge()
10
+ *
11
+ * // Reactive connection status
12
+ * watchEffect(() => {
13
+ * if (connected.value) {
14
+ * console.log('SSE connected')
15
+ * }
16
+ * })
17
+ *
18
+ * // Subscribe to specific event (auto-cleanup on unmount)
19
+ * onEvent('task:completed', (payload) => {
20
+ * console.log('Task completed:', payload.data)
21
+ * })
22
+ * ```
23
+ */
24
+
25
+ import { ref, inject, onUnmounted, type Ref } from 'vue'
26
+ import { SSE_SIGNALS } from '../kernel/SSEBridge'
27
+ import type { SignalBus } from '../kernel/SignalBus'
28
+
29
+ /**
30
+ * SSE Bridge interface
31
+ */
32
+ interface SSEBridgeInstance {
33
+ isConnected: () => boolean
34
+ isReconnecting: () => boolean
35
+ }
36
+
37
+ /**
38
+ * Signal payload with error
39
+ */
40
+ interface SSEErrorPayload {
41
+ error: string
42
+ }
43
+
44
+ /**
45
+ * Return type for useSSEBridge
46
+ */
47
+ export interface UseSSEBridgeReturn {
48
+ /** Connection status */
49
+ connected: Ref<boolean>
50
+ /** Reconnection in progress */
51
+ reconnecting: Ref<boolean>
52
+ /** Last error message */
53
+ error: Ref<string | null>
54
+ /** Subscribe to SSE event (auto-cleanup) */
55
+ onEvent: (eventName: string, handler: (payload: unknown) => void) => () => void
56
+ /** Subscribe to all SSE events (wildcard) */
57
+ onAnyEvent: (handler: (payload: unknown) => void) => () => void
58
+ }
59
+
60
+ /**
61
+ * Composable for SSEBridge connection status
62
+ * @returns SSE bridge state and methods
63
+ */
64
+ export function useSSEBridge(): UseSSEBridgeReturn {
65
+ const signals = inject<SignalBus | null>('qdadmSignals', null)
66
+ const sseBridge = inject<SSEBridgeInstance | null>('qdadmSSEBridge', null)
67
+
68
+ const connected = ref(sseBridge?.isConnected() ?? false)
69
+ const reconnecting = ref(sseBridge?.isReconnecting() ?? false)
70
+ const error = ref<string | null>(null)
71
+
72
+ const unbinders: Array<() => void> = []
73
+
74
+ // Track connection status via signals
75
+ if (signals) {
76
+ const unbindConnected = signals.on(SSE_SIGNALS.CONNECTED, () => {
77
+ connected.value = true
78
+ reconnecting.value = false
79
+ error.value = null
80
+ })
81
+ unbinders.push(unbindConnected)
82
+
83
+ const unbindDisconnected = signals.on(SSE_SIGNALS.DISCONNECTED, () => {
84
+ connected.value = false
85
+ })
86
+ unbinders.push(unbindDisconnected)
87
+
88
+ const unbindError = signals.on(SSE_SIGNALS.ERROR, (payload: unknown) => {
89
+ const errorPayload = payload as SSEErrorPayload
90
+ error.value = errorPayload.error
91
+ reconnecting.value = true
92
+ })
93
+ unbinders.push(unbindError)
94
+ }
95
+
96
+ /**
97
+ * Subscribe to a specific SSE event
98
+ * Automatically cleans up on component unmount.
99
+ *
100
+ * @param eventName - SSE event name (without 'sse:' prefix)
101
+ * @param handler - Event handler (payload) => void
102
+ * @returns Unbind function
103
+ */
104
+ const onEvent = (eventName: string, handler: (payload: unknown) => void): (() => void) => {
105
+ if (!signals) return () => {}
106
+
107
+ const signal = `sse:${eventName}`
108
+ const unbind = signals.on(signal, handler)
109
+ unbinders.push(unbind)
110
+
111
+ return unbind
112
+ }
113
+
114
+ /**
115
+ * Subscribe to all SSE events (wildcard)
116
+ * Automatically cleans up on component unmount.
117
+ *
118
+ * @param handler - Event handler (payload) => void
119
+ * @returns Unbind function
120
+ */
121
+ const onAnyEvent = (handler: (payload: unknown) => void): (() => void) => {
122
+ if (!signals) return () => {}
123
+
124
+ const unbind = signals.on('sse:*', handler)
125
+ unbinders.push(unbind)
126
+
127
+ return unbind
128
+ }
129
+
130
+ // Cleanup on unmount
131
+ onUnmounted(() => {
132
+ unbinders.forEach((unbind) => unbind())
133
+ })
134
+
135
+ return {
136
+ connected,
137
+ reconnecting,
138
+ error,
139
+ onEvent,
140
+ onAnyEvent,
141
+ }
142
+ }