@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,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZoneRegistry - Manages named zones and block registrations
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Twig/Symfony block system. Zones are named slots in layouts
|
|
5
|
+
* where blocks (components) can be injected with weight ordering.
|
|
6
|
+
*
|
|
7
|
+
* **Vue Reactivity**: The registry uses Vue's reactivity system internally.
|
|
8
|
+
* When blocks are added, removed, or modified, zones automatically trigger
|
|
9
|
+
* re-renders in components using them.
|
|
10
|
+
*
|
|
11
|
+
* Supports block operations:
|
|
12
|
+
* - 'add' (default): Simply add block to zone
|
|
13
|
+
* - 'replace': Substitute an existing block entirely
|
|
14
|
+
* - 'extend': Insert block before/after an existing block
|
|
15
|
+
* - 'wrap': Wrap an existing block with a decorator component
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```ts
|
|
19
|
+
* const registry = new ZoneRegistry()
|
|
20
|
+
*
|
|
21
|
+
* // Define zones with optional defaults
|
|
22
|
+
* registry.defineZone('header', { default: DefaultHeader })
|
|
23
|
+
* registry.defineZone('sidebar')
|
|
24
|
+
*
|
|
25
|
+
* // Register blocks with weight (lower = first)
|
|
26
|
+
* registry.registerBlock('header', { component: Logo, weight: 10, id: 'logo' })
|
|
27
|
+
* registry.registerBlock('header', { component: UserMenu, weight: 90 })
|
|
28
|
+
*
|
|
29
|
+
* // Replace a block entirely
|
|
30
|
+
* registry.registerBlock('header', {
|
|
31
|
+
* component: CustomLogo,
|
|
32
|
+
* operation: 'replace',
|
|
33
|
+
* replaces: 'logo'
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* // Extend: insert after an existing block
|
|
37
|
+
* registry.registerBlock('header', {
|
|
38
|
+
* component: ExtraMenuItem,
|
|
39
|
+
* operation: 'extend',
|
|
40
|
+
* after: 'logo'
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Extend: insert before an existing block
|
|
44
|
+
* registry.registerBlock('header', {
|
|
45
|
+
* component: Announcements,
|
|
46
|
+
* operation: 'extend',
|
|
47
|
+
* before: 'logo'
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* // Wrap: decorate a block with before/after content
|
|
51
|
+
* registry.registerBlock('header', {
|
|
52
|
+
* component: BorderWrapper,
|
|
53
|
+
* operation: 'wrap',
|
|
54
|
+
* wraps: 'logo'
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* // Unregister blocks at runtime
|
|
58
|
+
* registry.unregisterBlock('header', 'logo')
|
|
59
|
+
*
|
|
60
|
+
* // Get blocks sorted by weight
|
|
61
|
+
* registry.getBlocks('header') // [Announcements, CustomLogo@10, ExtraMenuItem, UserMenu@90]
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
import { shallowRef, type ShallowRef, type Component } from 'vue'
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Block operation types
|
|
69
|
+
*/
|
|
70
|
+
export type BlockOperation = 'add' | 'replace' | 'extend' | 'wrap'
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrapper info for wrapped blocks
|
|
74
|
+
*/
|
|
75
|
+
export interface WrapperInfo {
|
|
76
|
+
component: Component
|
|
77
|
+
props?: Record<string, unknown>
|
|
78
|
+
id?: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Block configuration
|
|
83
|
+
*/
|
|
84
|
+
export interface BlockConfig {
|
|
85
|
+
/** Vue component to render */
|
|
86
|
+
component: Component
|
|
87
|
+
/** Ordering weight (lower = first) */
|
|
88
|
+
weight?: number
|
|
89
|
+
/** Props to pass to component */
|
|
90
|
+
props?: Record<string, unknown>
|
|
91
|
+
/** Unique identifier for duplicate detection */
|
|
92
|
+
id?: string | null
|
|
93
|
+
/** Block operation */
|
|
94
|
+
operation?: BlockOperation
|
|
95
|
+
/** Block ID to replace (required if operation='replace') */
|
|
96
|
+
replaces?: string | null
|
|
97
|
+
/** Block ID to insert before (for operation='extend') */
|
|
98
|
+
before?: string | null
|
|
99
|
+
/** Block ID to insert after (for operation='extend') */
|
|
100
|
+
after?: string | null
|
|
101
|
+
/** Block ID to wrap (required if operation='wrap') */
|
|
102
|
+
wraps?: string | null
|
|
103
|
+
/** Wrappers applied to this block */
|
|
104
|
+
wrappers?: WrapperInfo[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Internal block representation with normalized fields
|
|
109
|
+
*/
|
|
110
|
+
interface InternalBlock {
|
|
111
|
+
component: Component
|
|
112
|
+
weight: number
|
|
113
|
+
props: Record<string, unknown>
|
|
114
|
+
id: string | null
|
|
115
|
+
operation: BlockOperation
|
|
116
|
+
replaces: string | null
|
|
117
|
+
before: string | null
|
|
118
|
+
after: string | null
|
|
119
|
+
wraps: string | null
|
|
120
|
+
wrappers?: WrapperInfo[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Zone configuration
|
|
125
|
+
*/
|
|
126
|
+
export interface ZoneConfig {
|
|
127
|
+
/** Default component if no blocks registered */
|
|
128
|
+
default: Component | null
|
|
129
|
+
/** Registered blocks */
|
|
130
|
+
blocks: InternalBlock[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Zone definition options
|
|
135
|
+
*/
|
|
136
|
+
export interface ZoneDefineOptions {
|
|
137
|
+
/** Default component when no blocks are registered */
|
|
138
|
+
default?: Component | null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Zone info for listing
|
|
143
|
+
*/
|
|
144
|
+
export interface ZoneListInfo {
|
|
145
|
+
name: string
|
|
146
|
+
blockCount: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Detailed zone info for debugging
|
|
151
|
+
*/
|
|
152
|
+
export interface ZoneDetailInfo {
|
|
153
|
+
name: string
|
|
154
|
+
hasDefault: boolean
|
|
155
|
+
blockCount: number
|
|
156
|
+
blocks: Array<{
|
|
157
|
+
id: string | null
|
|
158
|
+
weight: number
|
|
159
|
+
hasProps: boolean
|
|
160
|
+
}>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Zone inspection result
|
|
165
|
+
*/
|
|
166
|
+
export interface ZoneInspection {
|
|
167
|
+
name: string
|
|
168
|
+
blocks: Array<{
|
|
169
|
+
id: string | null
|
|
170
|
+
weight: number
|
|
171
|
+
component: string
|
|
172
|
+
wrappers?: Array<{
|
|
173
|
+
id: string | null
|
|
174
|
+
component: string
|
|
175
|
+
}>
|
|
176
|
+
}>
|
|
177
|
+
default: string | null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* ZoneRegistry creation options
|
|
182
|
+
*/
|
|
183
|
+
export interface ZoneRegistryOptions {
|
|
184
|
+
debug?: boolean
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const DEFAULT_WEIGHT = 50
|
|
188
|
+
|
|
189
|
+
export class ZoneRegistry {
|
|
190
|
+
/**
|
|
191
|
+
* Zone storage: name -> ZoneConfig
|
|
192
|
+
*/
|
|
193
|
+
private _zones: Map<string, ZoneConfig>
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Cache for sorted blocks (invalidated on registerBlock)
|
|
197
|
+
*/
|
|
198
|
+
private _sortedCache: Map<string, BlockConfig[]>
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Wrap graph for cycle detection: zoneName -> Map<wrapperId, targetId>
|
|
202
|
+
*/
|
|
203
|
+
private _wrapGraph: Map<string, Map<string, string>>
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Debug mode for warnings
|
|
207
|
+
*/
|
|
208
|
+
private _debug: boolean
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Version counter for reactivity - triggers re-renders when blocks change
|
|
212
|
+
* Using shallowRef for lightweight reactivity that tracks mutation count
|
|
213
|
+
*/
|
|
214
|
+
private _version: ShallowRef<number>
|
|
215
|
+
|
|
216
|
+
constructor() {
|
|
217
|
+
this._zones = new Map()
|
|
218
|
+
this._sortedCache = new Map()
|
|
219
|
+
this._wrapGraph = new Map()
|
|
220
|
+
this._debug = false
|
|
221
|
+
this._version = shallowRef(0)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get the reactive version ref for tracking changes
|
|
226
|
+
*
|
|
227
|
+
* Components can watch this ref to re-render when blocks change.
|
|
228
|
+
* Use `useZoneRegistry()` composable for convenient access.
|
|
229
|
+
*
|
|
230
|
+
* @returns Reactive version counter
|
|
231
|
+
*/
|
|
232
|
+
getVersionRef(): ShallowRef<number> {
|
|
233
|
+
return this._version
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Trigger Vue reactivity update
|
|
238
|
+
*
|
|
239
|
+
* Called after any mutation that should cause Zone components to re-render.
|
|
240
|
+
*/
|
|
241
|
+
private _triggerUpdate(): void {
|
|
242
|
+
this._version.value++
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Enable or disable debug mode
|
|
247
|
+
*
|
|
248
|
+
* When enabled, warnings are logged for missing replace targets, etc.
|
|
249
|
+
*
|
|
250
|
+
* @param enabled - Whether to enable debug mode
|
|
251
|
+
* @returns This instance for chaining
|
|
252
|
+
*/
|
|
253
|
+
setDebug(enabled: boolean): this {
|
|
254
|
+
this._debug = !!enabled
|
|
255
|
+
return this
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Define a new zone
|
|
260
|
+
*
|
|
261
|
+
* If zone already exists, merges options (updating default if provided).
|
|
262
|
+
*
|
|
263
|
+
* @param name - Zone name (e.g., 'header', 'sidebar')
|
|
264
|
+
* @param options - Zone options
|
|
265
|
+
* @returns This instance for chaining
|
|
266
|
+
*/
|
|
267
|
+
defineZone(name: string, options: ZoneDefineOptions = {}): this {
|
|
268
|
+
if (!name || typeof name !== 'string') {
|
|
269
|
+
throw new Error('[ZoneRegistry] Zone name must be a non-empty string')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const existing = this._zones.get(name)
|
|
273
|
+
if (existing) {
|
|
274
|
+
// Merge: update default if provided
|
|
275
|
+
if (options.default !== undefined) {
|
|
276
|
+
existing.default = options.default
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
this._zones.set(name, {
|
|
280
|
+
default: options.default ?? null,
|
|
281
|
+
blocks: [],
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return this
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Register a block in a zone
|
|
290
|
+
*
|
|
291
|
+
* Blocks are sorted by weight (ascending). Equal weights maintain insertion order.
|
|
292
|
+
*
|
|
293
|
+
* Operations:
|
|
294
|
+
* - 'add' (default): Simply add the block to the zone
|
|
295
|
+
* - 'replace': Substitute an existing block by ID
|
|
296
|
+
* - 'extend': Insert block before/after an existing block
|
|
297
|
+
* - 'wrap': Wrap an existing block with a decorator component
|
|
298
|
+
*
|
|
299
|
+
* @param zoneName - Target zone name
|
|
300
|
+
* @param blockConfig - Block configuration
|
|
301
|
+
* @returns This instance for chaining
|
|
302
|
+
* @throws Error If component is not provided
|
|
303
|
+
* @throws Error If operation is 'replace' but replaces is not specified
|
|
304
|
+
* @throws Error If operation is 'extend' but neither before nor after is specified
|
|
305
|
+
* @throws Error If operation is 'extend' and both before and after are specified
|
|
306
|
+
* @throws Error If operation is 'wrap' but wraps is not specified
|
|
307
|
+
* @throws Error If operation is 'wrap' but id is not specified
|
|
308
|
+
* @throws Error If wrap would create a circular dependency
|
|
309
|
+
*/
|
|
310
|
+
registerBlock(zoneName: string, blockConfig: BlockConfig): this {
|
|
311
|
+
if (!zoneName || typeof zoneName !== 'string') {
|
|
312
|
+
throw new Error('[ZoneRegistry] Zone name must be a non-empty string')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!blockConfig || !blockConfig.component) {
|
|
316
|
+
throw new Error('[ZoneRegistry] Block must have a component')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const operation = blockConfig.operation || 'add'
|
|
320
|
+
|
|
321
|
+
// Validate replace operation
|
|
322
|
+
if (operation === 'replace' && !blockConfig.replaces) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`[ZoneRegistry] Block with operation 'replace' must specify 'replaces' target ID`
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Validate extend operation
|
|
329
|
+
if (operation === 'extend') {
|
|
330
|
+
const hasBefore = !!blockConfig.before
|
|
331
|
+
const hasAfter = !!blockConfig.after
|
|
332
|
+
|
|
333
|
+
if (!hasBefore && !hasAfter) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`[ZoneRegistry] Block with operation 'extend' must specify either 'before' or 'after' target ID`
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
if (hasBefore && hasAfter) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`[ZoneRegistry] Block with operation 'extend' cannot specify both 'before' and 'after'`
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Validate wrap operation
|
|
346
|
+
if (operation === 'wrap') {
|
|
347
|
+
if (!blockConfig.wraps) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`[ZoneRegistry] Block with operation 'wrap' must specify 'wraps' target ID`
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
if (!blockConfig.id) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`[ZoneRegistry] Block with operation 'wrap' must have an 'id' for cycle detection`
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
// Check for circular wrap dependency
|
|
358
|
+
if (this._wouldCreateWrapCycle(zoneName, blockConfig.id, blockConfig.wraps)) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`[ZoneRegistry] Circular wrap dependency detected: "${blockConfig.id}" wrapping "${blockConfig.wraps}" would create a cycle`
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Auto-create zone if not exists (DX decision: reduce friction)
|
|
366
|
+
if (!this._zones.has(zoneName)) {
|
|
367
|
+
this.defineZone(zoneName)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const zone = this._zones.get(zoneName)!
|
|
371
|
+
|
|
372
|
+
// Prepare normalized block config
|
|
373
|
+
const block: InternalBlock = {
|
|
374
|
+
component: blockConfig.component,
|
|
375
|
+
weight: blockConfig.weight ?? DEFAULT_WEIGHT,
|
|
376
|
+
props: blockConfig.props ?? {},
|
|
377
|
+
id: blockConfig.id ?? null,
|
|
378
|
+
operation,
|
|
379
|
+
replaces: blockConfig.replaces ?? null,
|
|
380
|
+
before: blockConfig.before ?? null,
|
|
381
|
+
after: blockConfig.after ?? null,
|
|
382
|
+
wraps: blockConfig.wraps ?? null,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Update wrap graph for cycle detection (wrap operation only)
|
|
386
|
+
if (operation === 'wrap') {
|
|
387
|
+
if (!this._wrapGraph.has(zoneName)) {
|
|
388
|
+
this._wrapGraph.set(zoneName, new Map())
|
|
389
|
+
}
|
|
390
|
+
this._wrapGraph.get(zoneName)!.set(block.id!, block.wraps!)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check for duplicate ID
|
|
394
|
+
if (block.id) {
|
|
395
|
+
const existingIndex = zone.blocks.findIndex((b) => b.id === block.id)
|
|
396
|
+
if (existingIndex !== -1) {
|
|
397
|
+
// Warn in debug mode - duplicates shouldn't happen with proper module init
|
|
398
|
+
if (this._debug) {
|
|
399
|
+
console.warn(
|
|
400
|
+
`[qdadm:zones] Duplicate block ID "${block.id}" in zone "${zoneName}". ` +
|
|
401
|
+
`This may indicate a module is being initialized multiple times.`
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
// Replace existing block (backward compatibility)
|
|
405
|
+
zone.blocks[existingIndex] = block
|
|
406
|
+
} else {
|
|
407
|
+
zone.blocks.push(block)
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
zone.blocks.push(block)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Invalidate cache for this zone
|
|
414
|
+
this._sortedCache.delete(zoneName)
|
|
415
|
+
|
|
416
|
+
// Trigger Vue reactivity update
|
|
417
|
+
this._triggerUpdate()
|
|
418
|
+
|
|
419
|
+
// Dev mode logging for block registration
|
|
420
|
+
if (this._debug) {
|
|
421
|
+
const blockDesc = block.id || '(anonymous)'
|
|
422
|
+
const opDesc = operation === 'add' ? '' : ` [${operation}]`
|
|
423
|
+
console.debug(`[qdadm:zones] Registered block in zone: ${zoneName}, ${blockDesc}${opDesc}`)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return this
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Check if adding a wrap would create a cycle
|
|
431
|
+
*
|
|
432
|
+
* Detects cycles by traversing the wrap graph from the wrapper.
|
|
433
|
+
* If the target eventually wraps back to the wrapper, it's a cycle.
|
|
434
|
+
*
|
|
435
|
+
* @param zoneName - Zone name
|
|
436
|
+
* @param wrapperId - ID of the wrapper block being added
|
|
437
|
+
* @param targetId - ID of the block being wrapped
|
|
438
|
+
* @returns True if adding this wrap would create a cycle
|
|
439
|
+
*/
|
|
440
|
+
private _wouldCreateWrapCycle(zoneName: string, wrapperId: string, targetId: string): boolean {
|
|
441
|
+
// Self-wrap is a cycle
|
|
442
|
+
if (wrapperId === targetId) {
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const zoneGraph = this._wrapGraph.get(zoneName)
|
|
447
|
+
if (!zoneGraph) {
|
|
448
|
+
return false
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check if the target (or anything it wraps) eventually wraps the wrapper
|
|
452
|
+
// Start from targetId and follow the chain to see if we reach wrapperId
|
|
453
|
+
const visited = new Set<string>()
|
|
454
|
+
let current: string | undefined = targetId
|
|
455
|
+
|
|
456
|
+
while (current && !visited.has(current)) {
|
|
457
|
+
visited.add(current)
|
|
458
|
+
// What does 'current' wrap?
|
|
459
|
+
const wrapsWhat = zoneGraph.get(current)
|
|
460
|
+
if (wrapsWhat === wrapperId) {
|
|
461
|
+
// Found a cycle: target -> ... -> wrapper
|
|
462
|
+
return true
|
|
463
|
+
}
|
|
464
|
+
current = wrapsWhat
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get sorted blocks for a zone
|
|
472
|
+
*
|
|
473
|
+
* Returns blocks sorted by weight (ascending) after applying all block operations.
|
|
474
|
+
* Uses cached result if available.
|
|
475
|
+
*
|
|
476
|
+
* Operations:
|
|
477
|
+
* - 'add': Block is included directly
|
|
478
|
+
* - 'replace': Replaces target block, inherits target's weight if not specified
|
|
479
|
+
* - 'extend': Inserts block before/after target, uses target's weight for positioning
|
|
480
|
+
* - 'wrap': Adds wrappers array to target block containing wrapper components
|
|
481
|
+
*
|
|
482
|
+
* @param zoneName - Zone name
|
|
483
|
+
* @returns Sorted array of block configs (empty if zone undefined)
|
|
484
|
+
*/
|
|
485
|
+
getBlocks(zoneName: string): BlockConfig[] {
|
|
486
|
+
if (!this._zones.has(zoneName)) {
|
|
487
|
+
return []
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Return cached sorted result if available
|
|
491
|
+
if (this._sortedCache.has(zoneName)) {
|
|
492
|
+
return this._sortedCache.get(zoneName)!
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const zone = this._zones.get(zoneName)!
|
|
496
|
+
|
|
497
|
+
// Separate blocks by operation
|
|
498
|
+
const addBlocks: InternalBlock[] = []
|
|
499
|
+
const replaceBlocks: InternalBlock[] = []
|
|
500
|
+
const extendBlocks: InternalBlock[] = []
|
|
501
|
+
const wrapBlocks: InternalBlock[] = []
|
|
502
|
+
|
|
503
|
+
for (const block of zone.blocks) {
|
|
504
|
+
if (block.operation === 'replace') {
|
|
505
|
+
replaceBlocks.push(block)
|
|
506
|
+
} else if (block.operation === 'extend') {
|
|
507
|
+
extendBlocks.push(block)
|
|
508
|
+
} else if (block.operation === 'wrap') {
|
|
509
|
+
wrapBlocks.push(block)
|
|
510
|
+
} else {
|
|
511
|
+
addBlocks.push(block)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Start with add blocks
|
|
516
|
+
const result: InternalBlock[] = [...addBlocks]
|
|
517
|
+
|
|
518
|
+
// Apply replace operations first
|
|
519
|
+
for (const replaceBlock of replaceBlocks) {
|
|
520
|
+
const targetIndex = result.findIndex((b) => b.id === replaceBlock.replaces)
|
|
521
|
+
|
|
522
|
+
if (targetIndex === -1) {
|
|
523
|
+
// Target not found - warn but still add the replacement at its own weight
|
|
524
|
+
if (this._debug) {
|
|
525
|
+
console.warn(
|
|
526
|
+
`[ZoneRegistry] Replace target "${replaceBlock.replaces}" not found in zone "${zoneName}". ` +
|
|
527
|
+
`Adding replacement block at default weight.`
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
result.push(replaceBlock)
|
|
531
|
+
} else {
|
|
532
|
+
// Replace: use target's weight if replacement doesn't specify one
|
|
533
|
+
const targetBlock = result[targetIndex]!
|
|
534
|
+
const hasExplicitWeight = replaceBlock.weight !== DEFAULT_WEIGHT
|
|
535
|
+
const finalWeight = hasExplicitWeight ? replaceBlock.weight : targetBlock.weight
|
|
536
|
+
|
|
537
|
+
const replacement: InternalBlock = {
|
|
538
|
+
...replaceBlock,
|
|
539
|
+
weight: finalWeight,
|
|
540
|
+
}
|
|
541
|
+
result.splice(targetIndex, 1, replacement)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Sort by weight before applying extend operations
|
|
546
|
+
// This ensures targets are in their final positions
|
|
547
|
+
result.sort((a, b) => a.weight - b.weight)
|
|
548
|
+
|
|
549
|
+
// Apply extend operations (in registration order)
|
|
550
|
+
for (const extendBlock of extendBlocks) {
|
|
551
|
+
const targetId = extendBlock.before || extendBlock.after
|
|
552
|
+
const insertBefore = !!extendBlock.before
|
|
553
|
+
const targetIndex = result.findIndex((b) => b.id === targetId)
|
|
554
|
+
|
|
555
|
+
if (targetIndex === -1) {
|
|
556
|
+
// Target not found - warn and fall back to weight-based positioning
|
|
557
|
+
if (this._debug) {
|
|
558
|
+
console.warn(
|
|
559
|
+
`[ZoneRegistry] Extend target "${targetId}" not found in zone "${zoneName}". ` +
|
|
560
|
+
`Adding block at its own weight.`
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
// Insert at position based on weight
|
|
564
|
+
const insertIndex = result.findIndex((b) => b.weight > extendBlock.weight)
|
|
565
|
+
if (insertIndex === -1) {
|
|
566
|
+
result.push(extendBlock)
|
|
567
|
+
} else {
|
|
568
|
+
result.splice(insertIndex, 0, extendBlock)
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
// Insert before or after target
|
|
572
|
+
const insertIndex = insertBefore ? targetIndex : targetIndex + 1
|
|
573
|
+
result.splice(insertIndex, 0, extendBlock)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Apply wrap operations
|
|
578
|
+
// Build wrapper chains: for each target, collect all wrappers sorted by weight
|
|
579
|
+
if (wrapBlocks.length > 0) {
|
|
580
|
+
// Group wraps by target ID
|
|
581
|
+
const wrapsByTarget = new Map<string, InternalBlock[]>()
|
|
582
|
+
for (const wrapBlock of wrapBlocks) {
|
|
583
|
+
if (!wrapsByTarget.has(wrapBlock.wraps!)) {
|
|
584
|
+
wrapsByTarget.set(wrapBlock.wraps!, [])
|
|
585
|
+
}
|
|
586
|
+
wrapsByTarget.get(wrapBlock.wraps!)!.push(wrapBlock)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Collect IDs of blocks in result for orphan detection
|
|
590
|
+
const resultBlockIds = new Set(result.map((b) => b.id).filter(Boolean) as string[])
|
|
591
|
+
|
|
592
|
+
// Apply wrappers to blocks in result
|
|
593
|
+
for (let i = 0; i < result.length; i++) {
|
|
594
|
+
const block = result[i]
|
|
595
|
+
if (!block || !block.id) continue
|
|
596
|
+
|
|
597
|
+
// Collect all wrappers for this block (direct and nested)
|
|
598
|
+
const allWrappers = this._collectWrapChain(block.id, wrapsByTarget)
|
|
599
|
+
|
|
600
|
+
if (allWrappers.length > 0) {
|
|
601
|
+
// Sort wrappers by weight (lower weight = outer wrapper)
|
|
602
|
+
allWrappers.sort((a, b) => a.weight - b.weight)
|
|
603
|
+
|
|
604
|
+
result[i] = {
|
|
605
|
+
component: block.component,
|
|
606
|
+
weight: block.weight,
|
|
607
|
+
props: block.props,
|
|
608
|
+
id: block.id,
|
|
609
|
+
operation: block.operation,
|
|
610
|
+
replaces: block.replaces,
|
|
611
|
+
before: block.before,
|
|
612
|
+
after: block.after,
|
|
613
|
+
wraps: block.wraps,
|
|
614
|
+
wrappers: allWrappers.map((w) => ({
|
|
615
|
+
component: w.component,
|
|
616
|
+
props: w.props,
|
|
617
|
+
id: w.id,
|
|
618
|
+
})),
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Warn about orphaned wrappers (wrappers targeting non-existent blocks)
|
|
624
|
+
if (this._debug) {
|
|
625
|
+
for (const [targetId, wrappers] of wrapsByTarget.entries()) {
|
|
626
|
+
// Check if target exists in result blocks or as a wrapper ID
|
|
627
|
+
const wrapperIds = new Set(wrapBlocks.map((w) => w.id))
|
|
628
|
+
if (!resultBlockIds.has(targetId) && !wrapperIds.has(targetId)) {
|
|
629
|
+
for (const wrapper of wrappers) {
|
|
630
|
+
console.warn(
|
|
631
|
+
`[ZoneRegistry] Wrap target "${targetId}" not found in zone "${zoneName}". ` +
|
|
632
|
+
`Wrapper "${wrapper.id}" will be ignored.`
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Clean up internal operation fields for external consumers
|
|
641
|
+
const cleaned: BlockConfig[] = result.map(({ component, weight, props, id, wrappers }) => ({
|
|
642
|
+
component,
|
|
643
|
+
weight,
|
|
644
|
+
props,
|
|
645
|
+
id,
|
|
646
|
+
...(wrappers ? { wrappers } : {}),
|
|
647
|
+
}))
|
|
648
|
+
|
|
649
|
+
// Cache the result
|
|
650
|
+
this._sortedCache.set(zoneName, cleaned)
|
|
651
|
+
|
|
652
|
+
return cleaned
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Collect all wrappers for a block, including nested wrappers
|
|
657
|
+
*
|
|
658
|
+
* If WrapperA wraps MainContent and WrapperB wraps WrapperA,
|
|
659
|
+
* returns [WrapperA, WrapperB] (inner to outer before sorting)
|
|
660
|
+
*
|
|
661
|
+
* @param targetId - Block ID being wrapped
|
|
662
|
+
* @param wrapsByTarget - Map of target ID -> wrapper blocks
|
|
663
|
+
* @returns Array of wrapper block configs
|
|
664
|
+
*/
|
|
665
|
+
private _collectWrapChain(
|
|
666
|
+
targetId: string,
|
|
667
|
+
wrapsByTarget: Map<string, InternalBlock[]>
|
|
668
|
+
): InternalBlock[] {
|
|
669
|
+
const directWrappers = wrapsByTarget.get(targetId) || []
|
|
670
|
+
|
|
671
|
+
if (directWrappers.length === 0) {
|
|
672
|
+
return []
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Collect nested wrappers (wrappers of wrappers)
|
|
676
|
+
const allWrappers = [...directWrappers]
|
|
677
|
+
|
|
678
|
+
for (const wrapper of directWrappers) {
|
|
679
|
+
const nestedWrappers = this._collectWrapChain(wrapper.id!, wrapsByTarget)
|
|
680
|
+
allWrappers.push(...nestedWrappers)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return allWrappers
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Get zone default component
|
|
688
|
+
*
|
|
689
|
+
* @param zoneName - Zone name
|
|
690
|
+
* @returns Default component or null
|
|
691
|
+
*/
|
|
692
|
+
getDefault(zoneName: string): Component | null {
|
|
693
|
+
const zone = this._zones.get(zoneName)
|
|
694
|
+
return zone?.default ?? null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Check if a zone has any blocks registered
|
|
699
|
+
*
|
|
700
|
+
* @param zoneName - Zone name
|
|
701
|
+
* @returns Whether zone has blocks
|
|
702
|
+
*/
|
|
703
|
+
hasBlocks(zoneName: string): boolean {
|
|
704
|
+
const zone = this._zones.get(zoneName)
|
|
705
|
+
return zone ? zone.blocks.length > 0 : false
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Check if a zone is defined
|
|
710
|
+
*
|
|
711
|
+
* @param zoneName - Zone name
|
|
712
|
+
* @returns Whether zone exists
|
|
713
|
+
*/
|
|
714
|
+
hasZone(zoneName: string): boolean {
|
|
715
|
+
return this._zones.has(zoneName)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* List all defined zones with metadata
|
|
720
|
+
*
|
|
721
|
+
* Useful for debugging and introspection. Returns zone names with block counts.
|
|
722
|
+
*
|
|
723
|
+
* @returns Array of zone info objects
|
|
724
|
+
*/
|
|
725
|
+
listZones(): ZoneListInfo[] {
|
|
726
|
+
const result: ZoneListInfo[] = []
|
|
727
|
+
for (const [name, zone] of this._zones) {
|
|
728
|
+
result.push({
|
|
729
|
+
name,
|
|
730
|
+
blockCount: zone.blocks.length,
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
return result
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Get zone info for debugging
|
|
738
|
+
*
|
|
739
|
+
* @param zoneName - Zone name
|
|
740
|
+
* @returns Zone info or null if not defined
|
|
741
|
+
*/
|
|
742
|
+
getZoneInfo(zoneName: string): ZoneDetailInfo | null {
|
|
743
|
+
const zone = this._zones.get(zoneName)
|
|
744
|
+
if (!zone) return null
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
name: zoneName,
|
|
748
|
+
hasDefault: zone.default !== null,
|
|
749
|
+
blockCount: zone.blocks.length,
|
|
750
|
+
blocks: zone.blocks.map((b) => ({
|
|
751
|
+
id: b.id,
|
|
752
|
+
weight: b.weight,
|
|
753
|
+
hasProps: Object.keys(b.props).length > 0,
|
|
754
|
+
})),
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Inspect a zone for debugging - detailed view with component info
|
|
760
|
+
*
|
|
761
|
+
* Returns zone details including blocks with component names,
|
|
762
|
+
* suitable for DevTools console inspection.
|
|
763
|
+
*
|
|
764
|
+
* @param zoneName - Zone name
|
|
765
|
+
* @returns Detailed zone inspection or null if not defined
|
|
766
|
+
*/
|
|
767
|
+
inspect(zoneName: string): ZoneInspection | null {
|
|
768
|
+
const zone = this._zones.get(zoneName)
|
|
769
|
+
if (!zone) return null
|
|
770
|
+
|
|
771
|
+
// Get sorted blocks for accurate representation
|
|
772
|
+
const sortedBlocks = this.getBlocks(zoneName)
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
name: zoneName,
|
|
776
|
+
blocks: sortedBlocks.map((b) => ({
|
|
777
|
+
id: b.id ?? null,
|
|
778
|
+
weight: b.weight ?? DEFAULT_WEIGHT,
|
|
779
|
+
component: this._getComponentName(b.component),
|
|
780
|
+
...(b.wrappers
|
|
781
|
+
? {
|
|
782
|
+
wrappers: b.wrappers.map((w) => ({
|
|
783
|
+
id: w.id ?? null,
|
|
784
|
+
component: this._getComponentName(w.component),
|
|
785
|
+
})),
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
788
|
+
})),
|
|
789
|
+
default: zone.default ? this._getComponentName(zone.default) : null,
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Get a human-readable component name for debugging
|
|
795
|
+
*
|
|
796
|
+
* @param component - Vue component
|
|
797
|
+
* @returns Component name or fallback
|
|
798
|
+
*/
|
|
799
|
+
private _getComponentName(component: Component): string {
|
|
800
|
+
if (!component) return '(none)'
|
|
801
|
+
if (typeof component === 'string') return component
|
|
802
|
+
const comp = component as { name?: string; __name?: string }
|
|
803
|
+
return comp.name || comp.__name || '(anonymous)'
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Remove all blocks from a zone
|
|
808
|
+
*
|
|
809
|
+
* @param zoneName - Zone name
|
|
810
|
+
* @returns This instance for chaining
|
|
811
|
+
*/
|
|
812
|
+
clearZone(zoneName: string): this {
|
|
813
|
+
const zone = this._zones.get(zoneName)
|
|
814
|
+
if (zone) {
|
|
815
|
+
const hadBlocks = zone.blocks.length > 0
|
|
816
|
+
zone.blocks = []
|
|
817
|
+
this._sortedCache.delete(zoneName)
|
|
818
|
+
this._wrapGraph.delete(zoneName)
|
|
819
|
+
|
|
820
|
+
// Trigger Vue reactivity update only if we removed blocks
|
|
821
|
+
if (hadBlocks) {
|
|
822
|
+
this._triggerUpdate()
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return this
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Remove a specific block by ID
|
|
830
|
+
*
|
|
831
|
+
* Also available as `unregisterBlock()` for semantic clarity in runtime scenarios.
|
|
832
|
+
*
|
|
833
|
+
* @param zoneName - Zone name
|
|
834
|
+
* @param blockId - Block ID to remove
|
|
835
|
+
* @returns True if block was found and removed
|
|
836
|
+
*/
|
|
837
|
+
removeBlock(zoneName: string, blockId: string): boolean {
|
|
838
|
+
const zone = this._zones.get(zoneName)
|
|
839
|
+
if (!zone) {
|
|
840
|
+
if (this._debug) {
|
|
841
|
+
console.warn(`[ZoneRegistry] Cannot remove block "${blockId}": zone "${zoneName}" not found`)
|
|
842
|
+
}
|
|
843
|
+
return false
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const index = zone.blocks.findIndex((b) => b.id === blockId)
|
|
847
|
+
if (index === -1) {
|
|
848
|
+
if (this._debug) {
|
|
849
|
+
console.warn(`[ZoneRegistry] Block "${blockId}" not found in zone "${zoneName}"`)
|
|
850
|
+
}
|
|
851
|
+
return false
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const block = zone.blocks[index]!
|
|
855
|
+
zone.blocks.splice(index, 1)
|
|
856
|
+
this._sortedCache.delete(zoneName)
|
|
857
|
+
|
|
858
|
+
// Remove from wrap graph if it was a wrap operation
|
|
859
|
+
if (block.operation === 'wrap') {
|
|
860
|
+
const zoneGraph = this._wrapGraph.get(zoneName)
|
|
861
|
+
if (zoneGraph) {
|
|
862
|
+
zoneGraph.delete(blockId)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Trigger Vue reactivity update
|
|
867
|
+
this._triggerUpdate()
|
|
868
|
+
|
|
869
|
+
if (this._debug) {
|
|
870
|
+
console.debug(`[qdadm:zones] Unregistered block from zone: ${zoneName}, ${blockId}`)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return true
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Unregister a block from a zone at runtime
|
|
878
|
+
*
|
|
879
|
+
* Alias for `removeBlock()` with semantic clarity for runtime block management.
|
|
880
|
+
* Use this when dynamically adding/removing blocks based on user state, feature flags, etc.
|
|
881
|
+
*
|
|
882
|
+
* @param zoneName - Zone name
|
|
883
|
+
* @param blockId - Block ID to unregister
|
|
884
|
+
* @returns True if block was found and removed
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* // Register a block when component mounts
|
|
888
|
+
* onMounted(() => {
|
|
889
|
+
* registry.registerBlock('sidebar', { component: AdBanner, weight: 50, id: 'ad-banner' })
|
|
890
|
+
* })
|
|
891
|
+
*
|
|
892
|
+
* // Unregister when component unmounts
|
|
893
|
+
* onUnmounted(() => {
|
|
894
|
+
* registry.unregisterBlock('sidebar', 'ad-banner')
|
|
895
|
+
* })
|
|
896
|
+
*/
|
|
897
|
+
unregisterBlock(zoneName: string, blockId: string): boolean {
|
|
898
|
+
return this.removeBlock(zoneName, blockId)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Clear all zones and blocks
|
|
903
|
+
*
|
|
904
|
+
* Useful for testing.
|
|
905
|
+
*
|
|
906
|
+
* @returns This instance for chaining
|
|
907
|
+
*/
|
|
908
|
+
clear(): this {
|
|
909
|
+
const hadZones = this._zones.size > 0
|
|
910
|
+
this._zones.clear()
|
|
911
|
+
this._sortedCache.clear()
|
|
912
|
+
this._wrapGraph.clear()
|
|
913
|
+
|
|
914
|
+
// Trigger Vue reactivity update only if we had zones
|
|
915
|
+
if (hadZones) {
|
|
916
|
+
this._triggerUpdate()
|
|
917
|
+
}
|
|
918
|
+
return this
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Create a new ZoneRegistry instance
|
|
924
|
+
*
|
|
925
|
+
* @param options - Registry options
|
|
926
|
+
* @returns ZoneRegistry instance
|
|
927
|
+
*/
|
|
928
|
+
export function createZoneRegistry(options: ZoneRegistryOptions = {}): ZoneRegistry {
|
|
929
|
+
const registry = new ZoneRegistry()
|
|
930
|
+
if (options.debug) {
|
|
931
|
+
registry.setDebug(true)
|
|
932
|
+
}
|
|
933
|
+
return registry
|
|
934
|
+
}
|