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.
Files changed (86) hide show
  1. package/package.json +8 -4
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. 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
+ }
@@ -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
- root: Component
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
  /**
@@ -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
- // Module-level reference to debug bar component (set by constructor)
20
- let _QdadmDebugBar: Component | null = null
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 constructor)
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
- _QdadmDebugBar = component
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 && _QdadmDebugBar ? _QdadmDebugBar : null
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
- app.use(createPinia())
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
- app.use(this.router!)
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 exposed (orchestrator, signals, hooks, zones, deferred, router)'
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
+ }