qdadm 1.13.0 → 1.19.2
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/package.json +8 -4
- package/src/chain/ActiveStack.ts +79 -98
- package/src/chain/StackHydrator.ts +3 -2
- package/src/chain/index.ts +7 -1
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/edit/FormActions.vue +9 -6
- package/src/components/edit/LookupPickerDialog.vue +6 -3
- package/src/components/index.ts +6 -0
- package/src/composables/useEntityItemFormPage.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +1 -0
- package/src/composables/useFieldManager.ts +100 -3
- package/src/composables/useListPage.ts +50 -59
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavigation.ts +26 -3
- package/src/composables/useOptionsLookup.ts +5 -1
- package/src/gen/generateManagers.test.js +27 -0
- package/src/gen/generateManagers.ts +12 -0
- package/src/hooks/HookRegistry.ts +14 -435
- 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 +37 -0
- package/src/kernel/EventRouter.ts +17 -300
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +6 -0
- package/src/kernel/Kernel.registries.ts +10 -2
- package/src/kernel/Kernel.routing.ts +43 -1
- package/src/kernel/Kernel.ts +43 -0
- package/src/kernel/Kernel.types.ts +52 -1
- package/src/kernel/Kernel.vue.ts +121 -15
- 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 +125 -834
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/SSEBridge.ts +7 -362
- package/src/kernel/SignalBus.ts +24 -148
- package/src/modules/debug/AuthCollector.ts +48 -1
- package/src/modules/debug/Collector.ts +16 -302
- package/src/modules/debug/DebugBridge.ts +10 -171
- package/src/modules/debug/DebugModule.ts +35 -5
- package/src/modules/debug/EntitiesCollector.ts +97 -1
- package/src/modules/debug/ErrorCollector.ts +2 -77
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +3 -147
- package/src/modules/debug/RouterCollector.ts +101 -1
- package/src/modules/debug/SignalCollector.ts +2 -150
- package/src/modules/debug/ToastCollector.ts +2 -91
- package/src/modules/debug/ZonesCollector.ts +93 -1
- package/src/modules/debug/components/DebugBar.vue +19 -775
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +4 -3
- package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
- package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
- package/src/modules/debug/components/panels/index.ts +10 -4
- package/src/modules/debug/index.ts +15 -0
- package/src/modules/debug/styles.scss +22 -18
- package/src/utils/index.ts +0 -3
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +16 -0
- package/src/modules/debug/components/ObjectTree.vue +0 -123
- package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
- package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
- package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
- package/src/utils/debugInjector.ts +0 -306
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch Kernel prototype with i18n-related methods.
|
|
3
|
+
*
|
|
4
|
+
* Adds:
|
|
5
|
+
* - kernel.i18n : I18n instance
|
|
6
|
+
* - _createI18n() : init step
|
|
7
|
+
* - _bootstrapI18n() : warmup step (loads default + fallback locales)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { I18n } from '../i18n/I18n'
|
|
11
|
+
import type { I18nOptions } from '../i18n/types'
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type Self = any
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
export function applyI18nMethods(KernelClass: { prototype: any }): void {
|
|
18
|
+
const proto = KernelClass.prototype as Self
|
|
19
|
+
|
|
20
|
+
proto._createI18n = function (this: Self): void {
|
|
21
|
+
const opts = (this.options.i18n as I18nOptions | undefined) ?? {}
|
|
22
|
+
this.i18nInstance = new I18n(opts, { signals: this.signals })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
proto._bootstrapI18n = function (this: Self): Promise<void> {
|
|
26
|
+
if (!this.i18nInstance) return Promise.resolve()
|
|
27
|
+
return this.i18nInstance.bootstrap()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -15,6 +15,12 @@ export function applyModuleMethods(KernelClass: { prototype: any }): void {
|
|
|
15
15
|
* Fire entity cache warmups
|
|
16
16
|
*/
|
|
17
17
|
proto._fireWarmups = function (this: Self): void {
|
|
18
|
+
if (this.i18nInstance) {
|
|
19
|
+
// Fire-and-forget bootstrap; locale-sensitive pages await
|
|
20
|
+
// 'i18n:locale:{loc}:ready' via DeferredRegistry if needed.
|
|
21
|
+
void this._bootstrapI18n()
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const warmup = this.options.warmup ?? true
|
|
19
25
|
if (!warmup) return
|
|
20
26
|
|
|
@@ -24,9 +24,18 @@ export function applyRegistryMethods(KernelClass: { prototype: any }): void {
|
|
|
24
24
|
const proto = KernelClass.prototype as Self
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Create signal bus for event-driven communication
|
|
27
|
+
* Create signal bus for event-driven communication.
|
|
28
|
+
*
|
|
29
|
+
* When `options.existingSignals` is provided (host shell already
|
|
30
|
+
* owns a bus), reuse it instead of spinning up a fresh one. This
|
|
31
|
+
* is what lets qdcms and qdadm share entity events when they're
|
|
32
|
+
* mounted on the same Vue app.
|
|
28
33
|
*/
|
|
29
34
|
proto._createSignalBus = function (this: Self): void {
|
|
35
|
+
if (this.options.existingSignals) {
|
|
36
|
+
this.signals = this.options.existingSignals
|
|
37
|
+
return
|
|
38
|
+
}
|
|
30
39
|
const debug = this.options.debug ?? false
|
|
31
40
|
this.signals = createSignalBus({ debug })
|
|
32
41
|
}
|
|
@@ -189,7 +198,6 @@ export function applyRegistryMethods(KernelClass: { prototype: any }): void {
|
|
|
189
198
|
const debug = this.options.debug ?? false
|
|
190
199
|
this.eventRouter = createEventRouter({
|
|
191
200
|
signals: this.signals!,
|
|
192
|
-
orchestrator: this.orchestrator,
|
|
193
201
|
routes,
|
|
194
202
|
debug,
|
|
195
203
|
})
|
|
@@ -19,7 +19,14 @@ export function applyRoutingMethods(KernelClass: { prototype: any }): void {
|
|
|
19
19
|
const proto = KernelClass.prototype as Self
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Create Vue Router
|
|
22
|
+
* Create Vue Router.
|
|
23
|
+
*
|
|
24
|
+
* When `options.existingRouter` is provided, the Kernel reuses it
|
|
25
|
+
* and registers its computed routes via `addRoute()` instead of
|
|
26
|
+
* calling `createRouter()`. Combined with `options.routePrefix`,
|
|
27
|
+
* this lets a host mount the whole admin tree under a sub-path
|
|
28
|
+
* (e.g. `routePrefix: '/admin'` shifts the layout to `/admin` and
|
|
29
|
+
* the catch-all to `/admin/:pathMatch(.*)*`).
|
|
23
30
|
*/
|
|
24
31
|
proto._createRouter = function (this: Self): void {
|
|
25
32
|
const { pages, homeRoute, coreRoutes, basePath } = this.options
|
|
@@ -111,6 +118,26 @@ export function applyRoutingMethods(KernelClass: { prototype: any }): void {
|
|
|
111
118
|
|
|
112
119
|
routes.push(notFoundRoute)
|
|
113
120
|
|
|
121
|
+
// ──────────────────────────────────────────────────────────────
|
|
122
|
+
// routePrefix: prepend to every Kernel-built route so the whole
|
|
123
|
+
// tree mounts under a sub-path (host scenarios like a CMS that
|
|
124
|
+
// lives at `/` and hosts qdadm at `/admin`). Empty by default.
|
|
125
|
+
const prefix = (this.options.routePrefix ?? '').replace(/\/+$/, '')
|
|
126
|
+
if (prefix) {
|
|
127
|
+
routes = routes.map((r) => prefixRoute(r, prefix))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.options.existingRouter) {
|
|
131
|
+
// Host shell owns the router. Skip createRouter, register on
|
|
132
|
+
// the supplied instance. We do NOT touch history mode here —
|
|
133
|
+
// the host is responsible for that.
|
|
134
|
+
this.router = this.options.existingRouter
|
|
135
|
+
for (const r of routes) {
|
|
136
|
+
this.router.addRoute(r)
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
const { hashMode } = this.options
|
|
115
142
|
const history = hashMode
|
|
116
143
|
? createWebHashHistory(basePath)
|
|
@@ -290,3 +317,18 @@ export function applyRoutingMethods(KernelClass: { prototype: any }): void {
|
|
|
290
317
|
this.activeStack!.set(levels)
|
|
291
318
|
}
|
|
292
319
|
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Prepend a path prefix to a top-level route record. Children inherit
|
|
323
|
+
* relative paths so we only touch the outer record. Used by the
|
|
324
|
+
* `routePrefix` option to mount the whole admin tree under a sub-path
|
|
325
|
+
* of the host router.
|
|
326
|
+
*/
|
|
327
|
+
function prefixRoute(route: RouteRecordRaw, prefix: string): RouteRecordRaw {
|
|
328
|
+
if (!route.path) return route
|
|
329
|
+
// Normalise: ensure single leading slash.
|
|
330
|
+
const tail = route.path.startsWith('/') ? route.path : `/${route.path}`
|
|
331
|
+
// `/` becomes the prefix itself, otherwise prefix + tail.
|
|
332
|
+
const newPath = tail === '/' ? prefix : `${prefix}${tail}`
|
|
333
|
+
return { ...route, path: newPath }
|
|
334
|
+
}
|
package/src/kernel/Kernel.ts
CHANGED
|
@@ -75,6 +75,8 @@ import { applyModuleMethods } from './Kernel.modules'
|
|
|
75
75
|
import { applyRegistryMethods } from './Kernel.registries'
|
|
76
76
|
import { setQdadmDebugBar, applyVueMethods } from './Kernel.vue'
|
|
77
77
|
import { applyApiMethods } from './Kernel.api'
|
|
78
|
+
import { applyI18nMethods } from './Kernel.i18n'
|
|
79
|
+
import type { I18n } from '../i18n/I18n'
|
|
78
80
|
|
|
79
81
|
export class Kernel {
|
|
80
82
|
options: KernelOptions
|
|
@@ -94,6 +96,7 @@ export class Kernel {
|
|
|
94
96
|
activeStack: ActiveStack | null = null
|
|
95
97
|
stackHydrator: StackHydrator | null = null
|
|
96
98
|
notificationStore: NotificationStore | null = null
|
|
99
|
+
i18nInstance: I18n | null = null
|
|
97
100
|
|
|
98
101
|
/** Pending provides from modules (applied after vueApp creation) */
|
|
99
102
|
_pendingProvides: Map<string | symbol, unknown> = new Map()
|
|
@@ -152,6 +155,7 @@ export class Kernel {
|
|
|
152
155
|
createApp(): App {
|
|
153
156
|
this._resolveEntityAuthAdapter()
|
|
154
157
|
this._createSignalBus()
|
|
158
|
+
this._createI18n()
|
|
155
159
|
this._createHookRegistry()
|
|
156
160
|
this._createZoneRegistry()
|
|
157
161
|
this._createActiveStack()
|
|
@@ -185,6 +189,7 @@ export class Kernel {
|
|
|
185
189
|
async createAppAsync(): Promise<App> {
|
|
186
190
|
this._resolveEntityAuthAdapter()
|
|
187
191
|
this._createSignalBus()
|
|
192
|
+
this._createI18n()
|
|
188
193
|
this._createHookRegistry()
|
|
189
194
|
this._createZoneRegistry()
|
|
190
195
|
this._createActiveStack()
|
|
@@ -355,6 +360,39 @@ export class Kernel {
|
|
|
355
360
|
const debugModule = this.getDebugModule()
|
|
356
361
|
return debugModule?.getBridge?.() ?? null
|
|
357
362
|
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* I18n subsystem accessor
|
|
366
|
+
*/
|
|
367
|
+
get i18n(): I18n | null {
|
|
368
|
+
return this.i18nInstance
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Build the merged context every qdadm debug collector expects.
|
|
373
|
+
*
|
|
374
|
+
* Used by hosts that own a shared `DebugBridge` (passed via
|
|
375
|
+
* `debugBar.bridge`) and need to install it themselves with a
|
|
376
|
+
* context covering both qdadm internals and host-side data. Hosts
|
|
377
|
+
* should `Object.assign({}, kernel.getDebugContext(), { … })` to
|
|
378
|
+
* weave in their own keys (e.g. cms) before calling
|
|
379
|
+
* `bridge.install()`.
|
|
380
|
+
*
|
|
381
|
+
* Returning a flat object keeps the surface stable as Kernel
|
|
382
|
+
* internals evolve — the host pokes nothing.
|
|
383
|
+
*/
|
|
384
|
+
getDebugContext(): Record<string, unknown> {
|
|
385
|
+
return {
|
|
386
|
+
signals: this.signals,
|
|
387
|
+
router: this.router,
|
|
388
|
+
orchestrator: this.orchestrator,
|
|
389
|
+
zones: this.zoneRegistry,
|
|
390
|
+
hooks: this.hookRegistry,
|
|
391
|
+
activeStack: this.activeStack,
|
|
392
|
+
stackHydrator: this.stackHydrator,
|
|
393
|
+
i18n: this.i18nInstance,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
358
396
|
}
|
|
359
397
|
|
|
360
398
|
// Declare prototype-patched methods
|
|
@@ -383,6 +421,10 @@ export interface Kernel {
|
|
|
383
421
|
_wireModulesAsync(): Promise<void>
|
|
384
422
|
_fireWarmups(): void
|
|
385
423
|
|
|
424
|
+
// i18n (Kernel.i18n.ts)
|
|
425
|
+
_createI18n(): void
|
|
426
|
+
_bootstrapI18n(): Promise<void>
|
|
427
|
+
|
|
386
428
|
// Registries (Kernel.registries.ts)
|
|
387
429
|
_createSignalBus(): void
|
|
388
430
|
_createHookRegistry(): void
|
|
@@ -415,3 +457,4 @@ applyModuleMethods(Kernel)
|
|
|
415
457
|
applyRegistryMethods(Kernel)
|
|
416
458
|
applyVueMethods(Kernel)
|
|
417
459
|
applyApiMethods(Kernel)
|
|
460
|
+
applyI18nMethods(Kernel)
|
|
@@ -10,6 +10,7 @@ import type { RoutesConfig } from './EventRouter'
|
|
|
10
10
|
import type { EntityManager } from '../entity/EntityManager'
|
|
11
11
|
import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
|
|
12
12
|
import type { RoleProvider } from '../security/RolesProvider'
|
|
13
|
+
import type { I18nOptions } from '../i18n/types'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Auth adapter interface (app-level authentication)
|
|
@@ -115,6 +116,15 @@ export interface DebugBarConfig {
|
|
|
115
116
|
component?: Component
|
|
116
117
|
enabled?: boolean
|
|
117
118
|
_kernelManaged?: boolean
|
|
119
|
+
/**
|
|
120
|
+
* Optional shared `DebugBridge` to register qdadm collectors onto.
|
|
121
|
+
* When provided, the host shell owns the bridge (collectors from
|
|
122
|
+
* other frameworks like qdcms can be added too) and is responsible
|
|
123
|
+
* for calling `bridge.install(mergedCtx)` once everything is
|
|
124
|
+
* registered. Without it, the DebugModule creates and installs its
|
|
125
|
+
* own bridge as before.
|
|
126
|
+
*/
|
|
127
|
+
bridge?: unknown
|
|
118
128
|
[key: string]: unknown
|
|
119
129
|
}
|
|
120
130
|
|
|
@@ -135,7 +145,12 @@ export type HomeRoute = string | { name?: string; component?: Component }
|
|
|
135
145
|
* Kernel options
|
|
136
146
|
*/
|
|
137
147
|
export interface KernelOptions {
|
|
138
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Root component the Kernel should mount. Required UNLESS
|
|
150
|
+
* `existingApp` is provided (host shell already created and owns
|
|
151
|
+
* the Vue app). Validated at runtime in `_createVueApp`.
|
|
152
|
+
*/
|
|
153
|
+
root?: Component
|
|
139
154
|
modules?: Record<string, unknown>
|
|
140
155
|
modulesOptions?: Record<string, unknown>
|
|
141
156
|
moduleDefs?: unknown[]
|
|
@@ -167,6 +182,42 @@ export interface KernelOptions {
|
|
|
167
182
|
toast?: Record<string, unknown>
|
|
168
183
|
debug?: boolean
|
|
169
184
|
onAuthExpired?: (payload: unknown) => void
|
|
185
|
+
i18n?: I18nOptions
|
|
186
|
+
/**
|
|
187
|
+
* Existing Vue app to install qdadm onto. When provided, the Kernel
|
|
188
|
+
* skips its own `createApp(WrappedRoot)` and reuses the supplied
|
|
189
|
+
* instance — useful when a host shell (e.g. a CMS) already owns the
|
|
190
|
+
* Vue app and wants qdadm as a guest. The host is responsible for
|
|
191
|
+
* rendering qdadm's DOM extras (debug bar, toast root); see
|
|
192
|
+
* `<QdadmRoot />` for a drop-in helper.
|
|
193
|
+
*
|
|
194
|
+
* Pinia / PrimeVue / vue-router are still installed by the Kernel
|
|
195
|
+
* unless the host has already done so — re-install is idempotent for
|
|
196
|
+
* the standard plugins.
|
|
197
|
+
*/
|
|
198
|
+
existingApp?: import('vue').App
|
|
199
|
+
/**
|
|
200
|
+
* Existing vue-router to register Kernel routes onto. When provided,
|
|
201
|
+
* the Kernel skips `createRouter()` and adds its computed routes via
|
|
202
|
+
* `existingRouter.addRoute(...)` instead. Combine with `routePrefix`
|
|
203
|
+
* to mount qdadm under a sub-path of the host (e.g. `/admin`).
|
|
204
|
+
*/
|
|
205
|
+
existingRouter?: import('vue-router').Router
|
|
206
|
+
/**
|
|
207
|
+
* Existing SignalBus to use instead of creating one. Critical when
|
|
208
|
+
* the host already runs a SignalBus that other frameworks (e.g.
|
|
209
|
+
* qdcms) emit on / listen to — sharing the bus is what makes
|
|
210
|
+
* cross-framework reactivity work.
|
|
211
|
+
*/
|
|
212
|
+
existingSignals?: SignalBus
|
|
213
|
+
/**
|
|
214
|
+
* Prefix prepended to every route the Kernel registers. Use with
|
|
215
|
+
* `existingRouter` to mount the whole admin tree under a sub-path
|
|
216
|
+
* (e.g. `routePrefix: '/admin'` makes the layout-children land at
|
|
217
|
+
* `/admin/home`, `/admin/users`, etc., and the catch-all becomes
|
|
218
|
+
* `/admin/:pathMatch(.*)*`). Default: '' (no prefix — root mount).
|
|
219
|
+
*/
|
|
220
|
+
routePrefix?: string
|
|
170
221
|
}
|
|
171
222
|
|
|
172
223
|
/**
|
package/src/kernel/Kernel.vue.ts
CHANGED
|
@@ -2,8 +2,10 @@ import {
|
|
|
2
2
|
createApp,
|
|
3
3
|
h,
|
|
4
4
|
defineComponent,
|
|
5
|
+
ref,
|
|
5
6
|
type App,
|
|
6
7
|
type Component,
|
|
8
|
+
type Ref,
|
|
7
9
|
} from 'vue'
|
|
8
10
|
import { createPinia } from 'pinia'
|
|
9
11
|
import ToastService from 'primevue/toastservice'
|
|
@@ -13,17 +15,43 @@ import Toast from 'primevue/toast'
|
|
|
13
15
|
import ToastListener from '../toast/ToastListener.vue'
|
|
14
16
|
import { createQdadm } from '../plugin.js'
|
|
15
17
|
import { createNotificationStore, NOTIFICATION_KEY } from '../notifications/NotificationStore'
|
|
18
|
+
import { I18N_INJECTION_KEY } from '../i18n/useI18n'
|
|
16
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
20
|
type Self = any
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Reactive reference to the debug bar component, set by the Kernel
|
|
24
|
+
* constructor when `debugBar.component` is provided.
|
|
25
|
+
*
|
|
26
|
+
* Why a window-stashed ref instead of a plain module-level export:
|
|
27
|
+
* Vite's HMR can fragment a single source module into multiple live
|
|
28
|
+
* instances (each importer getting a different `?t=…` version after
|
|
29
|
+
* HMR), so a module-level `const ref` is NOT shared across
|
|
30
|
+
* importers. Stashing the ref on `window.__qdadmDebugBarRef` (and
|
|
31
|
+
* lazy-initialising) is the only way to guarantee a single instance
|
|
32
|
+
* across all callers in dev. The Kernel writes via
|
|
33
|
+
* `setQdadmDebugBar()`; `<QdadmRoot />` reads via the same getter.
|
|
34
|
+
*/
|
|
35
|
+
const QDADM_DEBUG_BAR_GLOBAL_KEY = '__qdadmDebugBarRef'
|
|
36
|
+
|
|
37
|
+
export function getQdadmDebugBarRef(): Ref<Component | null> {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const w = typeof window !== 'undefined' ? (window as any) : null
|
|
40
|
+
if (w && w[QDADM_DEBUG_BAR_GLOBAL_KEY]) return w[QDADM_DEBUG_BAR_GLOBAL_KEY]
|
|
41
|
+
const r = ref<Component | null>(null)
|
|
42
|
+
if (w) w[QDADM_DEBUG_BAR_GLOBAL_KEY] = r
|
|
43
|
+
return r
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Back-compat alias — same instance as `getQdadmDebugBarRef()`. */
|
|
47
|
+
export const qdadmDebugBarRef: Ref<Component | null> = getQdadmDebugBarRef()
|
|
21
48
|
|
|
22
49
|
/**
|
|
23
|
-
* Set the debug bar component reference (called from Kernel
|
|
50
|
+
* Set the debug bar component reference (called from Kernel
|
|
51
|
+
* constructor). Equivalent to `getQdadmDebugBarRef().value = component`.
|
|
24
52
|
*/
|
|
25
53
|
export function setQdadmDebugBar(component: Component | null): void {
|
|
26
|
-
|
|
54
|
+
getQdadmDebugBarRef().value = component
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
@@ -47,16 +75,33 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
/**
|
|
50
|
-
* Create Vue app instance
|
|
78
|
+
* Create Vue app instance.
|
|
79
|
+
*
|
|
80
|
+
* When `options.existingApp` is provided, the Kernel reuses the
|
|
81
|
+
* host's app and skips its own `createApp(WrappedRoot)`. The host
|
|
82
|
+
* is then responsible for rendering qdadm's DOM extras (Toast,
|
|
83
|
+
* ToastListener, DebugBar) — see `<QdadmRoot />` for a drop-in
|
|
84
|
+
* helper that does it.
|
|
85
|
+
*
|
|
86
|
+
* `options.root` is ignored in this mode (the host already mounted
|
|
87
|
+
* its own root).
|
|
51
88
|
*/
|
|
52
89
|
proto._createVueApp = function (this: Self): void {
|
|
90
|
+
if (this.options.existingApp) {
|
|
91
|
+
this.vueApp = this.options.existingApp
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
if (!this.options.root) {
|
|
54
|
-
throw new Error('[Kernel] root component is required')
|
|
96
|
+
throw new Error('[Kernel] root component is required (or pass options.existingApp)')
|
|
55
97
|
}
|
|
56
98
|
|
|
57
99
|
const OriginalRoot = this.options.root
|
|
100
|
+
const debugBarRef = getQdadmDebugBarRef()
|
|
58
101
|
const DebugBarComponent =
|
|
59
|
-
this.options.debugBar?.component &&
|
|
102
|
+
this.options.debugBar?.component && debugBarRef.value
|
|
103
|
+
? debugBarRef.value
|
|
104
|
+
: null
|
|
60
105
|
const hasPrimeVue = !!this.options.primevue?.plugin
|
|
61
106
|
const appKey = this._appKey
|
|
62
107
|
|
|
@@ -88,15 +133,32 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
88
133
|
}
|
|
89
134
|
|
|
90
135
|
/**
|
|
91
|
-
* Install all plugins on Vue app
|
|
136
|
+
* Install all plugins on Vue app.
|
|
137
|
+
*
|
|
138
|
+
* When a host shell uses `existingApp`, it may have already
|
|
139
|
+
* installed Pinia / PrimeVue / vue-router. Calling `app.use(plugin)`
|
|
140
|
+
* twice is harmless for stateless plugins (vue-router, Pinia
|
|
141
|
+
* recognise the duplicate and no-op), but PrimeVue does not — guard
|
|
142
|
+
* with `_qdadmPluginsInstalled` markers on globalProperties so the
|
|
143
|
+
* Kernel never double-installs PrimeVue, ToastService, or
|
|
144
|
+
* ConfirmationService.
|
|
92
145
|
*/
|
|
93
146
|
proto._installPlugins = function (this: Self): void {
|
|
94
147
|
const app = this.vueApp!
|
|
95
|
-
const { authAdapter, features, primevue } = this.options
|
|
148
|
+
const { authAdapter, features, primevue, existingApp } = this.options
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
const marks = (app.config.globalProperties as any).__qdadmInstalled ??= {
|
|
151
|
+
pinia: false,
|
|
152
|
+
primevue: false,
|
|
153
|
+
router: false,
|
|
154
|
+
}
|
|
96
155
|
|
|
97
|
-
|
|
156
|
+
if (!marks.pinia) {
|
|
157
|
+
app.use(createPinia())
|
|
158
|
+
marks.pinia = true
|
|
159
|
+
}
|
|
98
160
|
|
|
99
|
-
if (primevue?.plugin) {
|
|
161
|
+
if (primevue?.plugin && !marks.primevue) {
|
|
100
162
|
const pvConfig = {
|
|
101
163
|
theme: {
|
|
102
164
|
preset: primevue.theme,
|
|
@@ -110,9 +172,20 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
110
172
|
app.use(ToastService)
|
|
111
173
|
app.use(ConfirmationService)
|
|
112
174
|
app.directive('tooltip', Tooltip)
|
|
175
|
+
marks.primevue = true
|
|
113
176
|
}
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
// Skip vue-router install when the host owned the router from the
|
|
179
|
+
// start — they already called `app.use(router)`. Calling it again
|
|
180
|
+
// emits Vue's "Plugin has already been applied" warning. When
|
|
181
|
+
// Kernel created the router itself, install is required.
|
|
182
|
+
if (!marks.router && !this.options.existingRouter) {
|
|
183
|
+
app.use(this.router!)
|
|
184
|
+
marks.router = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Avoid eslint unused-var warning when host owns the app
|
|
188
|
+
void existingApp
|
|
116
189
|
|
|
117
190
|
for (const [key, value] of this._pendingProvides) {
|
|
118
191
|
app.provide(key, value)
|
|
@@ -133,6 +206,31 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
133
206
|
app.provide('qdadmStackHydrator', this.stackHydrator)
|
|
134
207
|
|
|
135
208
|
if (this.options.debug && typeof window !== 'undefined') {
|
|
209
|
+
const self = this
|
|
210
|
+
// Single namespaced surface for browser-console & agent introspection.
|
|
211
|
+
// The `debug` sub-object proxies to whatever DebugBridge the DebugModule
|
|
212
|
+
// installed (resolved lazily so we don't depend on module load order).
|
|
213
|
+
const debugProxy = {
|
|
214
|
+
get bridge() {
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
+
const debugMod = (self as any).moduleLoader?.getModules?.()?.get?.('debug')
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
218
|
+
return debugMod?.getBridge?.() ?? (debugMod as any)?._bridge ?? null
|
|
219
|
+
},
|
|
220
|
+
describe(): unknown {
|
|
221
|
+
const b = this.bridge
|
|
222
|
+
return b ? b.describe() : null
|
|
223
|
+
},
|
|
224
|
+
dump(): unknown {
|
|
225
|
+
const b = this.bridge
|
|
226
|
+
return b ? b.dump() : null
|
|
227
|
+
},
|
|
228
|
+
call(name: string, action: string, args: Record<string, unknown> = {}): Promise<unknown> {
|
|
229
|
+
const b = this.bridge
|
|
230
|
+
if (!b) return Promise.reject(new Error('[qdadm] DebugBridge unavailable'))
|
|
231
|
+
return b.call(name, action, args)
|
|
232
|
+
},
|
|
233
|
+
}
|
|
136
234
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
235
|
;(window as any).__qdadm = {
|
|
138
236
|
kernel: this,
|
|
@@ -144,18 +242,23 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
144
242
|
stackHydrator: this.stackHydrator,
|
|
145
243
|
deferred: this.deferred,
|
|
146
244
|
router: this.router,
|
|
245
|
+
i18n: this.i18nInstance,
|
|
147
246
|
get: (name: string) => this.orchestrator!.get(name),
|
|
148
247
|
managers: () => this.orchestrator!.getRegisteredNames(),
|
|
248
|
+
debug: debugProxy,
|
|
149
249
|
}
|
|
150
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
-
;(window as any).__qdadmZones = this.zoneRegistry
|
|
152
250
|
console.debug(
|
|
153
|
-
'[qdadm] Debug mode: window.__qdadm
|
|
251
|
+
'[qdadm] Debug mode: window.__qdadm.{kernel,orchestrator,signals,hooks,zones,router,i18n,debug.{describe,dump,call}}'
|
|
154
252
|
)
|
|
155
253
|
}
|
|
156
254
|
|
|
157
255
|
app.provide('qdadmSignals', this.signals)
|
|
158
256
|
|
|
257
|
+
if (this.i18nInstance) {
|
|
258
|
+
app.provide(I18N_INJECTION_KEY, this.i18nInstance)
|
|
259
|
+
app.provide('qdadmI18n', this.i18nInstance)
|
|
260
|
+
}
|
|
261
|
+
|
|
159
262
|
if (this.sseBridge) {
|
|
160
263
|
app.provide('qdadmSSEBridge', this.sseBridge)
|
|
161
264
|
}
|
|
@@ -163,6 +266,9 @@ export function applyVueMethods(KernelClass: { prototype: any }): void {
|
|
|
163
266
|
app.provide('qdadmHooks', this.hookRegistry)
|
|
164
267
|
app.provide('qdadmDeferred', this.deferred)
|
|
165
268
|
app.provide('qdadmLayoutComponents', this.layoutComponents)
|
|
269
|
+
// Surface PrimeVue presence so host-rendered <QdadmRoot /> can
|
|
270
|
+
// decide whether to mount Toast / ToastListener.
|
|
271
|
+
app.provide('qdadmHasPrimeVue', !!primevue?.plugin)
|
|
166
272
|
|
|
167
273
|
if (this.securityChecker) {
|
|
168
274
|
app.provide('qdadmSecurityChecker', this.securityChecker)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with entity registration methods:
|
|
3
|
+
* - entity(name, config) — generic EntityManager registration
|
|
4
|
+
* - userEntity(options) — UsersManager registration with conventions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { managerFactory, type ManagerFactoryContext } from '../entity/factory.js'
|
|
8
|
+
import type { EntityManager } from '../entity/EntityManager'
|
|
9
|
+
import { UsersManager } from '../security/UsersManager'
|
|
10
|
+
import type { UserEntityOptions } from './KernelContext.types'
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
type Self = any
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
export function applyEntityMethods(KernelContextClass: { prototype: any }): void {
|
|
17
|
+
const proto = KernelContextClass.prototype as Self
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register an entity manager.
|
|
21
|
+
*
|
|
22
|
+
* Uses managerFactory to resolve config into a manager instance.
|
|
23
|
+
* Registers with Orchestrator for CRUD operations.
|
|
24
|
+
*/
|
|
25
|
+
proto.entity = function (
|
|
26
|
+
this: Self,
|
|
27
|
+
name: string,
|
|
28
|
+
config: string | Record<string, unknown> | EntityManager
|
|
29
|
+
): Self {
|
|
30
|
+
const factoryContext = {
|
|
31
|
+
storageResolver: this._kernel.options.storageResolver,
|
|
32
|
+
managerResolver: this._kernel.options.managerResolver,
|
|
33
|
+
managerRegistry: this._kernel.options.managerRegistry || {},
|
|
34
|
+
} as ManagerFactoryContext
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const manager = managerFactory(config as any, name, factoryContext)
|
|
38
|
+
|
|
39
|
+
if (this._kernel.orchestrator) {
|
|
40
|
+
this._kernel.orchestrator.register(name, manager)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this._kernel.permissionRegistry) {
|
|
44
|
+
this._kernel.permissionRegistry.registerEntity(name, {
|
|
45
|
+
module: (this._module?.constructor as { name?: string })?.name || 'unknown',
|
|
46
|
+
// Register entity-own:* permissions if manager has isOwn configured
|
|
47
|
+
hasOwnership: !!(manager as unknown as { _isOwn?: unknown })._isOwn,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Track entity → module for the 'module' i18n key strategy
|
|
52
|
+
if (this._kernel.i18nInstance) {
|
|
53
|
+
const moduleName = (this._module?.constructor as { name?: string })?.name
|
|
54
|
+
if (moduleName) {
|
|
55
|
+
this._kernel.i18nInstance.registerEntityModule(name, moduleName)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a users entity with standard fields and role linking.
|
|
64
|
+
*/
|
|
65
|
+
proto.userEntity = function (this: Self, options: UserEntityOptions): Self {
|
|
66
|
+
const manager = new UsersManager(options)
|
|
67
|
+
|
|
68
|
+
if (this._kernel.orchestrator) {
|
|
69
|
+
this._kernel.orchestrator.register('users', manager)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this._kernel.permissionRegistry) {
|
|
73
|
+
this._kernel.permissionRegistry.registerEntity('users', {
|
|
74
|
+
module: (this._module?.constructor as { name?: string })?.name || 'unknown',
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return this
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with event-bus / hook / deferred methods:
|
|
3
|
+
* - on(signal, handler, options) — signal subscription, auto-cleaned on
|
|
4
|
+
* module disconnect
|
|
5
|
+
* - hook(hookName, handler, options) — hook registration
|
|
6
|
+
* - defer(name, factory) — queue a deferred task
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ListenerOptions } from '@quazardous/quarkernel'
|
|
10
|
+
import type {
|
|
11
|
+
DeferredFactory,
|
|
12
|
+
HookHandler,
|
|
13
|
+
SignalHandler,
|
|
14
|
+
} from './KernelContext.types'
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
type Self = any
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
export function applyEventMethods(KernelContextClass: { prototype: any }): void {
|
|
21
|
+
const proto = KernelContextClass.prototype as Self
|
|
22
|
+
|
|
23
|
+
proto.on = function (
|
|
24
|
+
this: Self,
|
|
25
|
+
signal: string,
|
|
26
|
+
handler: SignalHandler,
|
|
27
|
+
options: ListenerOptions = {}
|
|
28
|
+
): Self {
|
|
29
|
+
if (this._kernel.signals) {
|
|
30
|
+
const cleanup = this._kernel.signals.on(signal, handler, options)
|
|
31
|
+
// Register cleanup with module for automatic unsubscribe on disconnect
|
|
32
|
+
if (this._module && typeof this._module._addSignalCleanup === 'function') {
|
|
33
|
+
this._module._addSignalCleanup(cleanup)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
proto.hook = function (
|
|
40
|
+
this: Self,
|
|
41
|
+
hookName: string,
|
|
42
|
+
handler: HookHandler,
|
|
43
|
+
options: Record<string, unknown> = {}
|
|
44
|
+
): Self {
|
|
45
|
+
if (this._kernel.hookRegistry) {
|
|
46
|
+
this._kernel.hookRegistry.register(hookName, handler, options)
|
|
47
|
+
}
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
proto.defer = function (this: Self, name: string, factory: DeferredFactory): Self {
|
|
52
|
+
if (this._kernel.deferred) {
|
|
53
|
+
this._kernel.deferred.queue(name, factory)
|
|
54
|
+
}
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
}
|