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