@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,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module - User session authentication
|
|
3
|
+
*
|
|
4
|
+
* For entity-level permissions, see entity/auth/EntityAuthAdapter
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
SessionAuthAdapter,
|
|
9
|
+
LocalStorageSessionAuthAdapter,
|
|
10
|
+
type LoginCredentials,
|
|
11
|
+
type SessionData,
|
|
12
|
+
type AuthUser,
|
|
13
|
+
type ISessionAuthAdapter,
|
|
14
|
+
type LocalStorageAuthConfig,
|
|
15
|
+
} from './SessionAuthAdapter'
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActiveStack - Sync navigation context from route (qdadm flavour).
|
|
3
|
+
*
|
|
4
|
+
* Composes a generic `Stack<EntityStackLevel>` from `@quazardous/qdcore` and
|
|
5
|
+
* adds qdadm-specific lookups (e.g. `getLevelByEntity`). The underlying levels
|
|
6
|
+
* dual-carry `name` (qdcore-aligned) and `entity` (legacy qdadm field), so all
|
|
7
|
+
* existing consumers continue to read `level.entity` unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Stack only contains levels WITH IDs (entities with context).
|
|
10
|
+
* - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
|
|
11
|
+
* - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
|
|
12
|
+
*
|
|
13
|
+
* Signals emitted (via SignalBus):
|
|
14
|
+
* - stack:change — when stack levels change
|
|
15
|
+
*
|
|
16
|
+
* For Vue reactivity, use the `useActiveStack` composable.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const stack = new ActiveStack(signalBus)
|
|
20
|
+
* signalBus.on('stack:change', ({ levels }) => console.log('Stack:', levels))
|
|
21
|
+
* stack.set([{ entity: 'bots', param: 'uuid', foreignKey: null, id: 'bot-123' }])
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Stack, type ContentStackLevel } from '@quazardous/qdcore'
|
|
25
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* qdadm stack level: entity-bound `ContentStackLevel`.
|
|
29
|
+
*
|
|
30
|
+
* `name` and `entity` always carry the same value. `name` is the qdcore-canonical
|
|
31
|
+
* field; `entity` is preserved for backwards compatibility with qdadm callers.
|
|
32
|
+
*/
|
|
33
|
+
export interface EntityStackLevel extends ContentStackLevel {
|
|
34
|
+
type: 'entity'
|
|
35
|
+
/** qdcore-aligned name (= entity) */
|
|
36
|
+
name: string
|
|
37
|
+
/** Entity name (e.g., 'bots', 'commands') — alias of `name` */
|
|
38
|
+
entity: string
|
|
39
|
+
/** Route param name (e.g., 'uuid', 'id') */
|
|
40
|
+
param: string
|
|
41
|
+
/** Foreign key field for parent relation (null for root) */
|
|
42
|
+
foreignKey: string | null
|
|
43
|
+
/** Entity ID from route params */
|
|
44
|
+
id: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legacy alias kept for backwards compatibility. New code should prefer
|
|
49
|
+
* {@link EntityStackLevel}.
|
|
50
|
+
*/
|
|
51
|
+
export type StackLevel = EntityStackLevel
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Input shape accepted by {@link ActiveStack.set} — `name` is optional and
|
|
55
|
+
* defaults to `entity` so existing callers (`{ entity, param, foreignKey, id }`)
|
|
56
|
+
* keep working without changes.
|
|
57
|
+
*/
|
|
58
|
+
export type EntityStackLevelInput = Omit<EntityStackLevel, 'type' | 'name'> & {
|
|
59
|
+
type?: 'entity'
|
|
60
|
+
name?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stack change event payload
|
|
65
|
+
*/
|
|
66
|
+
export interface StackChangePayload {
|
|
67
|
+
levels: EntityStackLevel[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normalize loose input into a fully-typed EntityStackLevel.
|
|
72
|
+
*/
|
|
73
|
+
function normalize(input: EntityStackLevelInput): EntityStackLevel {
|
|
74
|
+
return {
|
|
75
|
+
type: 'entity',
|
|
76
|
+
name: input.name ?? input.entity,
|
|
77
|
+
entity: input.entity,
|
|
78
|
+
param: input.param,
|
|
79
|
+
foreignKey: input.foreignKey,
|
|
80
|
+
id: input.id,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ActiveStack {
|
|
85
|
+
private _stack: Stack<EntityStackLevel>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param signalBus - Optional signal bus for events
|
|
89
|
+
*/
|
|
90
|
+
constructor(signalBus: SignalBus | null = null) {
|
|
91
|
+
this._stack = new Stack<EntityStackLevel>(signalBus)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Mutators ─────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Replace entire stack (called on route change).
|
|
98
|
+
* Only emits if levels actually changed (handled by qdcore Stack).
|
|
99
|
+
* Accepts either fully-typed `EntityStackLevel` or the legacy
|
|
100
|
+
* `{ entity, param, foreignKey, id }` shape.
|
|
101
|
+
*/
|
|
102
|
+
set(levels: EntityStackLevelInput[]): void {
|
|
103
|
+
this._stack.set(levels.map(normalize))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear the stack.
|
|
108
|
+
*/
|
|
109
|
+
clear(): void {
|
|
110
|
+
this._stack.clear()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Accessors ────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
getLevels(): EntityStackLevel[] {
|
|
116
|
+
return this._stack.getLevels()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getLevel(index: number): EntityStackLevel | null {
|
|
120
|
+
return this._stack.getLevel(index)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get level by entity name (qdadm-specific helper).
|
|
125
|
+
*/
|
|
126
|
+
getLevelByEntity(entity: string): EntityStackLevel | null {
|
|
127
|
+
return this._stack.getLevels().find(l => l.entity === entity) ?? null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getCurrent(): EntityStackLevel | null {
|
|
131
|
+
return this._stack.getCurrent()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getParent(): EntityStackLevel | null {
|
|
135
|
+
return this._stack.getParent()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getRoot(): EntityStackLevel | null {
|
|
139
|
+
return this._stack.getRoot()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getDepth(): number {
|
|
143
|
+
return this._stack.getDepth()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Underlying qdcore Stack instance (escape hatch for advanced use).
|
|
148
|
+
*/
|
|
149
|
+
getCoreStack(): Stack<EntityStackLevel> {
|
|
150
|
+
return this._stack
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default ActiveStack
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StackHydrator - Async hydration layer for ActiveStack
|
|
3
|
+
*
|
|
4
|
+
* Pure vanilla JS using SignalBus for events.
|
|
5
|
+
* Listens to stack:change and hydrates each level with entity data.
|
|
6
|
+
*
|
|
7
|
+
* Signals emitted:
|
|
8
|
+
* - stack:hydrated - when hydration completes (batched, emits once after all levels) { levels }
|
|
9
|
+
*
|
|
10
|
+
* For Vue reactivity, use useStackHydrator composable.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const hydrator = new StackHydrator(activeStack, orchestrator, signalBus)
|
|
14
|
+
* signalBus.on('stack:hydrated', (event) => console.log('Hydrated:', event.data.levels))
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Hydrator } from '@quazardous/qdcore'
|
|
18
|
+
import type { ActiveStack, EntityStackLevel, StackLevel } from './ActiveStack'
|
|
19
|
+
import type { Orchestrator } from '../orchestrator/Orchestrator'
|
|
20
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
21
|
+
import type { EntityRecord } from '../types'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hydrated level with async state
|
|
25
|
+
*/
|
|
26
|
+
export interface HydratedLevel {
|
|
27
|
+
/** Entity name */
|
|
28
|
+
entity: string
|
|
29
|
+
/** Route param name */
|
|
30
|
+
param: string
|
|
31
|
+
/** Foreign key field */
|
|
32
|
+
foreignKey: string | null
|
|
33
|
+
/** Entity ID */
|
|
34
|
+
id: string | null
|
|
35
|
+
/** Resolves when hydrated */
|
|
36
|
+
promise: Promise<void> | null
|
|
37
|
+
/** True while fetching */
|
|
38
|
+
loading: boolean
|
|
39
|
+
/** True when data is loaded */
|
|
40
|
+
hydrated: boolean
|
|
41
|
+
/** Error if fetch failed */
|
|
42
|
+
error: Error | null
|
|
43
|
+
/** Entity data */
|
|
44
|
+
data: EntityRecord | null
|
|
45
|
+
/** Resolved label */
|
|
46
|
+
label: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hydrated levels payload
|
|
51
|
+
*/
|
|
52
|
+
export interface HydratedPayload {
|
|
53
|
+
levels: HydratedLevel[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class StackHydrator implements Hydrator<EntityStackLevel, HydratedLevel> {
|
|
57
|
+
private _activeStack: ActiveStack
|
|
58
|
+
private _orchestrator: Orchestrator
|
|
59
|
+
private _signalBus: SignalBus | null
|
|
60
|
+
private _levels: HydratedLevel[] = []
|
|
61
|
+
private _pendingEmit: Promise<void> | null = null
|
|
62
|
+
private _unsubscribe: (() => void) | null = null
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
activeStack: ActiveStack,
|
|
66
|
+
orchestrator: Orchestrator,
|
|
67
|
+
signalBus: SignalBus | null = null
|
|
68
|
+
) {
|
|
69
|
+
this._activeStack = activeStack
|
|
70
|
+
this._orchestrator = orchestrator
|
|
71
|
+
this._signalBus = signalBus
|
|
72
|
+
|
|
73
|
+
// Listen to stack changes via SignalBus
|
|
74
|
+
// QuarKernel passes (event, ctx) where event.data contains the payload
|
|
75
|
+
if (signalBus) {
|
|
76
|
+
this._unsubscribe = signalBus.on('stack:change', (event) => {
|
|
77
|
+
const levels = (event.data as { levels?: StackLevel[] })?.levels
|
|
78
|
+
if (levels) {
|
|
79
|
+
this._onStackChange(levels)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
// Note: No initial check needed - stack is always empty at construction time
|
|
84
|
+
// The Kernel's router.afterEach will trigger stack:change after navigation
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Cleanup
|
|
89
|
+
*/
|
|
90
|
+
destroy(): void {
|
|
91
|
+
if (this._unsubscribe) {
|
|
92
|
+
this._unsubscribe()
|
|
93
|
+
this._unsubscribe = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
// Stack change handling
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle stack change - rebuild hydrated levels
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
private _onStackChange(stackLevels: StackLevel[]): void {
|
|
106
|
+
// Create new hydrated levels (each starts its own async hydration)
|
|
107
|
+
this._levels = stackLevels.map((level, index) =>
|
|
108
|
+
this._createHydratedLevel(level, index)
|
|
109
|
+
)
|
|
110
|
+
// Don't emit here - each level will emit when hydration completes
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a hydrated level with its own promise
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
private _createHydratedLevel(stackLevel: StackLevel, _index: number): HydratedLevel {
|
|
118
|
+
const level: HydratedLevel = {
|
|
119
|
+
// Sync config (copied from stack)
|
|
120
|
+
entity: stackLevel.entity,
|
|
121
|
+
param: stackLevel.param,
|
|
122
|
+
foreignKey: stackLevel.foreignKey,
|
|
123
|
+
id: stackLevel.id,
|
|
124
|
+
|
|
125
|
+
// Async state
|
|
126
|
+
promise: null,
|
|
127
|
+
loading: false,
|
|
128
|
+
hydrated: false,
|
|
129
|
+
error: null,
|
|
130
|
+
data: null,
|
|
131
|
+
label: null,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Start hydration and store promise
|
|
135
|
+
level.promise = this._hydrate(level)
|
|
136
|
+
|
|
137
|
+
return level
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hydrate a single level
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
private async _hydrate(level: HydratedLevel): Promise<void> {
|
|
145
|
+
// All levels in stack have IDs (that's the new contract)
|
|
146
|
+
// But check anyway for safety
|
|
147
|
+
if (!level.id) {
|
|
148
|
+
const manager = this._orchestrator?.get(level.entity)
|
|
149
|
+
level.label = manager?.labelPlural ?? level.entity
|
|
150
|
+
level.hydrated = true
|
|
151
|
+
this._scheduleEmit()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
level.loading = true
|
|
156
|
+
// Don't emit on loading start - reduces signal noise
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const manager = this._orchestrator?.get(level.entity)
|
|
160
|
+
if (!manager) {
|
|
161
|
+
level.label = level.id
|
|
162
|
+
level.hydrated = true
|
|
163
|
+
level.loading = false
|
|
164
|
+
this._scheduleEmit()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fetch entity data
|
|
169
|
+
level.data = await manager.get(level.id)
|
|
170
|
+
level.label = manager.getEntityLabel?.(level.data) ?? level.id
|
|
171
|
+
level.hydrated = true
|
|
172
|
+
} catch (err) {
|
|
173
|
+
level.error = err instanceof Error ? err : new Error(String(err))
|
|
174
|
+
level.label = level.id // Fallback to ID
|
|
175
|
+
level.hydrated = true // Mark as hydrated even on error
|
|
176
|
+
} finally {
|
|
177
|
+
level.loading = false
|
|
178
|
+
this._scheduleEmit()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Schedule batched hydration signal (groups multiple level hydrations)
|
|
184
|
+
* Uses microtask to batch emissions within same tick
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
private _scheduleEmit(): void {
|
|
188
|
+
if (this._pendingEmit) return // Already scheduled
|
|
189
|
+
|
|
190
|
+
this._pendingEmit = Promise.resolve().then(() => {
|
|
191
|
+
this._pendingEmit = null
|
|
192
|
+
this._emit('stack:hydrated', { levels: this._levels })
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
+
// Accessors
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* All hydrated levels
|
|
202
|
+
*/
|
|
203
|
+
getLevels(): HydratedLevel[] {
|
|
204
|
+
return this._levels
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get hydrated level by index
|
|
209
|
+
*/
|
|
210
|
+
getLevel(index: number): HydratedLevel | null {
|
|
211
|
+
return this._levels[index] ?? null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get hydrated level by entity name
|
|
216
|
+
*/
|
|
217
|
+
getLevelByEntity(entity: string): HydratedLevel | null {
|
|
218
|
+
return this._levels.find(l => l.entity === entity) ?? null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Current hydrated level
|
|
223
|
+
*/
|
|
224
|
+
getCurrent(): HydratedLevel | null {
|
|
225
|
+
return this._levels.at(-1) ?? null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Parent hydrated level
|
|
230
|
+
*/
|
|
231
|
+
getParent(): HydratedLevel | null {
|
|
232
|
+
return this._levels.at(-2) ?? null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Root hydrated level
|
|
237
|
+
*/
|
|
238
|
+
getRoot(): HydratedLevel | null {
|
|
239
|
+
return this._levels[0] ?? null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
243
|
+
// Manual update (for pages that load their own data)
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Manually set data for current level (skips fetch)
|
|
248
|
+
* Used by pages that already loaded the entity
|
|
249
|
+
*/
|
|
250
|
+
setCurrentData(data: EntityRecord): void {
|
|
251
|
+
const level = this.getCurrent()
|
|
252
|
+
if (!level) return
|
|
253
|
+
|
|
254
|
+
const manager = this._orchestrator?.get(level.entity)
|
|
255
|
+
level.data = data
|
|
256
|
+
level.label = manager?.getEntityLabel?.(data) ?? level.id
|
|
257
|
+
level.hydrated = true
|
|
258
|
+
level.loading = false
|
|
259
|
+
this._scheduleEmit()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Manually set data for a level by entity name
|
|
264
|
+
*/
|
|
265
|
+
setEntityData(entity: string, data: EntityRecord): void {
|
|
266
|
+
const level = this.getLevelByEntity(entity)
|
|
267
|
+
if (!level) return
|
|
268
|
+
|
|
269
|
+
const manager = this._orchestrator?.get(entity)
|
|
270
|
+
level.data = data
|
|
271
|
+
level.label = manager?.getEntityLabel?.(data) ?? level.id
|
|
272
|
+
level.hydrated = true
|
|
273
|
+
level.loading = false
|
|
274
|
+
this._scheduleEmit()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
+
// Internal
|
|
279
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Emit event via SignalBus
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
private _emit(signal: string, payload: HydratedPayload): void {
|
|
286
|
+
if (this._signalBus) {
|
|
287
|
+
this._signalBus.emit(signal, payload)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export default StackHydrator
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain Module - Active navigation stack management
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - ActiveStack: Sync container for navigation context (entity, param, foreignKey, id)
|
|
6
|
+
* - StackHydrator: Async layer for entity data and labels
|
|
7
|
+
* - useActiveStack: Composable to build and access the sync stack
|
|
8
|
+
* - useStackHydrator: Composable to access hydrated data
|
|
9
|
+
*
|
|
10
|
+
* @module chain
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Sync stack (context only)
|
|
14
|
+
export {
|
|
15
|
+
ActiveStack,
|
|
16
|
+
type StackLevel,
|
|
17
|
+
type EntityStackLevel,
|
|
18
|
+
type EntityStackLevelInput,
|
|
19
|
+
type StackChangePayload,
|
|
20
|
+
} from './ActiveStack'
|
|
21
|
+
export { useActiveStack, type UseActiveStackReturn } from './useActiveStack'
|
|
22
|
+
|
|
23
|
+
// Async hydration (data + labels)
|
|
24
|
+
export { StackHydrator, type HydratedLevel, type HydratedPayload } from './StackHydrator'
|
|
25
|
+
export { useStackHydrator, type StackHydratorReturn } from './useStackHydrator'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useActiveStack - Access sync navigation stack
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive access to the navigation stack levels.
|
|
5
|
+
* Stack is rebuilt by Kernel on route change (single source of truth).
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Stack only contains levels WITH IDs.
|
|
8
|
+
* - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
|
|
9
|
+
* - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
|
|
10
|
+
*
|
|
11
|
+
* For hydrated data (labels, entity data), use useStackHydrator.
|
|
12
|
+
*
|
|
13
|
+
* Listens to SignalBus events for reactivity:
|
|
14
|
+
* - stack:change - when stack levels change
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { inject, ref, computed, onUnmounted, type ComputedRef, type Ref } from 'vue'
|
|
18
|
+
import type { ActiveStack, StackLevel } from './ActiveStack'
|
|
19
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return type for useActiveStack composable
|
|
23
|
+
*/
|
|
24
|
+
export interface UseActiveStackReturn {
|
|
25
|
+
/** Reactive levels array */
|
|
26
|
+
levels: Ref<StackLevel[]>
|
|
27
|
+
/** Current (deepest) level */
|
|
28
|
+
current: ComputedRef<StackLevel | null>
|
|
29
|
+
/** Parent level */
|
|
30
|
+
parent: ComputedRef<StackLevel | null>
|
|
31
|
+
/** Root level */
|
|
32
|
+
root: ComputedRef<StackLevel | null>
|
|
33
|
+
/** Stack depth */
|
|
34
|
+
depth: ComputedRef<number>
|
|
35
|
+
/** Get level by index */
|
|
36
|
+
getLevel: (index: number) => StackLevel | null
|
|
37
|
+
/** Get level by entity name */
|
|
38
|
+
getLevelByEntity: (entity: string) => StackLevel | null
|
|
39
|
+
/** Get current level (sync) */
|
|
40
|
+
getCurrent: () => StackLevel | null
|
|
41
|
+
/** Get parent level (sync) */
|
|
42
|
+
getParent: () => StackLevel | null
|
|
43
|
+
/** Get all levels (sync) */
|
|
44
|
+
getLevels: () => StackLevel[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useActiveStack(): UseActiveStackReturn {
|
|
48
|
+
const activeStack = inject<ActiveStack | undefined>('qdadmActiveStack')
|
|
49
|
+
const signalBus = inject<SignalBus | undefined>('qdadmSignals')
|
|
50
|
+
|
|
51
|
+
if (!activeStack) {
|
|
52
|
+
console.warn('[useActiveStack] ActiveStack not provided')
|
|
53
|
+
return createEmptyStack()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Reactive state - updated via SignalBus
|
|
57
|
+
const levels = ref<StackLevel[]>([...activeStack.getLevels()])
|
|
58
|
+
|
|
59
|
+
// Subscribe to stack changes
|
|
60
|
+
// QuarKernel passes (event, ctx) where event.data contains the payload
|
|
61
|
+
let unsubscribe: (() => void) | null = null
|
|
62
|
+
if (signalBus) {
|
|
63
|
+
unsubscribe = signalBus.on('stack:change', (event) => {
|
|
64
|
+
const newLevels = (event.data as { levels?: StackLevel[] })?.levels
|
|
65
|
+
if (newLevels) {
|
|
66
|
+
// Create new array reference to trigger reactivity
|
|
67
|
+
levels.value = [...newLevels]
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Cleanup on unmount
|
|
73
|
+
onUnmounted(() => {
|
|
74
|
+
if (unsubscribe) {
|
|
75
|
+
unsubscribe()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Computed accessors
|
|
80
|
+
const current = computed(() => levels.value.at(-1) ?? null)
|
|
81
|
+
const parent = computed(() => levels.value.at(-2) ?? null)
|
|
82
|
+
const root = computed(() => levels.value[0] ?? null)
|
|
83
|
+
const depth = computed(() => levels.value.length)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
// Computed refs (reactive)
|
|
87
|
+
levels,
|
|
88
|
+
current,
|
|
89
|
+
parent,
|
|
90
|
+
root,
|
|
91
|
+
depth,
|
|
92
|
+
|
|
93
|
+
// Sync accessors (direct from vanilla JS instance)
|
|
94
|
+
getLevel: (i: number) => activeStack.getLevel(i),
|
|
95
|
+
getLevelByEntity: (e: string) => activeStack.getLevelByEntity(e),
|
|
96
|
+
getCurrent: () => activeStack.getCurrent(),
|
|
97
|
+
getParent: () => activeStack.getParent(),
|
|
98
|
+
getLevels: () => activeStack.getLevels(),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fallback when ActiveStack not available
|
|
104
|
+
*/
|
|
105
|
+
function createEmptyStack(): UseActiveStackReturn {
|
|
106
|
+
return {
|
|
107
|
+
levels: ref([]),
|
|
108
|
+
current: computed(() => null),
|
|
109
|
+
parent: computed(() => null),
|
|
110
|
+
root: computed(() => null),
|
|
111
|
+
depth: computed(() => 0),
|
|
112
|
+
getLevel: () => null,
|
|
113
|
+
getLevelByEntity: () => null,
|
|
114
|
+
getCurrent: () => null,
|
|
115
|
+
getParent: () => null,
|
|
116
|
+
getLevels: () => [],
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default useActiveStack
|