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
@@ -11,7 +11,14 @@
11
11
  * collector.install(ctx)
12
12
  */
13
13
 
14
- import { Collector, type CollectorContext, type CollectorEntry, type CollectorOptions } from './Collector'
14
+ import {
15
+ Collector,
16
+ type CollectorContext,
17
+ type CollectorEntry,
18
+ type CollectorManifest,
19
+ type CollectorOptions,
20
+ type CollectorSnapshot,
21
+ } from './Collector'
15
22
  import { computeSemanticBreadcrumb } from '../../composables/useSemanticBreadcrumb'
16
23
  import type { Router, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
17
24
  import type { ActiveStack, StackLevel } from '../../chain/ActiveStack'
@@ -111,6 +118,8 @@ export class RouterCollector extends Collector<NavigationEntry> {
111
118
  private _guardInstalled: boolean = false
112
119
  private _activeStack: ActiveStack | null = null
113
120
  private _stackHydrator: StackHydrator | null = null
121
+ private _removeHydratedListener: (() => void) | null = null
122
+ private _removeStackChangeListener: (() => void) | null = null
114
123
 
115
124
  constructor(options: RouterCollectorOptions = {}) {
116
125
  super(options)
@@ -135,6 +144,20 @@ export class RouterCollector extends Collector<NavigationEntry> {
135
144
  this._activeStack = routerCtx.activeStack ?? null
136
145
  this._stackHydrator = routerCtx.stackHydrator ?? null
137
146
  // Router guard will be installed lazily when router becomes available
147
+
148
+ // Re-render the panel when the active stack changes or finishes hydrating —
149
+ // otherwise level.label stays on its loading placeholder until the next
150
+ // navigation triggers reactivity. afterEach (the navigation guard) only
151
+ // fires when the route changes, not when the async hydrator resolves.
152
+ const signals = ctx.signals
153
+ if (signals) {
154
+ this._removeStackChangeListener = signals.on('stack:change', () => {
155
+ this.notifyChange()
156
+ })
157
+ this._removeHydratedListener = signals.on('stack:hydrated', () => {
158
+ this.notifyChange()
159
+ })
160
+ }
138
161
  }
139
162
 
140
163
  /**
@@ -213,6 +236,14 @@ export class RouterCollector extends Collector<NavigationEntry> {
213
236
  this._removeGuard()
214
237
  this._removeGuard = null
215
238
  }
239
+ if (this._removeStackChangeListener) {
240
+ this._removeStackChangeListener()
241
+ this._removeStackChangeListener = null
242
+ }
243
+ if (this._removeHydratedListener) {
244
+ this._removeHydratedListener()
245
+ this._removeHydratedListener = null
246
+ }
216
247
  this._guardInstalled = false
217
248
  this._ctx = null
218
249
  this._history = []
@@ -346,4 +377,73 @@ export class RouterCollector extends Collector<NavigationEntry> {
346
377
  if (!this._router) return
347
378
  await this._router.push(path)
348
379
  }
380
+
381
+ override describe(): CollectorManifest {
382
+ return {
383
+ name: this.name,
384
+ records: true,
385
+ summary:
386
+ 'Captures navigations (afterEach) and exposes the current route, registered routes and the live ActiveStack.',
387
+ entryShape: {
388
+ id: 'number',
389
+ to: 'SerializedRoute',
390
+ from: 'SerializedRoute?',
391
+ seen: 'boolean',
392
+ timestamp: 'number',
393
+ },
394
+ stateShape: {
395
+ currentRoute: 'SerializedRoute?',
396
+ routes: 'RouteListEntry[]',
397
+ breadcrumb: 'unknown[]',
398
+ activeStack: 'ActiveStackInfo?',
399
+ },
400
+ actions: [
401
+ ...this._builtinActionManifests(),
402
+ {
403
+ name: 'navigate',
404
+ summary: 'Push a path onto the router (router.push).',
405
+ args: { path: 'string' },
406
+ mutates: true,
407
+ },
408
+ {
409
+ name: 'getCurrentRoute',
410
+ summary: 'Return the serialized current route.',
411
+ },
412
+ {
413
+ name: 'getRoutes',
414
+ summary: 'List every registered route.',
415
+ },
416
+ {
417
+ name: 'getBreadcrumb',
418
+ summary: 'Return the semantic breadcrumb for the current route.',
419
+ },
420
+ ],
421
+ }
422
+ }
423
+
424
+ override snapshot(): CollectorSnapshot {
425
+ return {
426
+ name: this.name,
427
+ entries: this._history.map((e) => ({ ...e })),
428
+ count: this._history.length,
429
+ unseen: this.getBadge(),
430
+ state: {
431
+ currentRoute: this.getCurrentRoute(),
432
+ routes: this.getRoutes(),
433
+ breadcrumb: this.getBreadcrumb(),
434
+ activeStack: this.getActiveStack(),
435
+ },
436
+ }
437
+ }
438
+
439
+ override async call(actionName: string, args: Record<string, unknown> = {}): Promise<unknown> {
440
+ if (actionName === 'navigate') {
441
+ await this.navigate(String(args.path ?? ''))
442
+ return { ok: true }
443
+ }
444
+ if (actionName === 'getCurrentRoute') return this.getCurrentRoute()
445
+ if (actionName === 'getRoutes') return this.getRoutes()
446
+ if (actionName === 'getBreadcrumb') return this.getBreadcrumb()
447
+ return super.call(actionName, args)
448
+ }
349
449
  }
@@ -1,152 +1,4 @@
1
1
  /**
2
- * SignalCollector - Debug collector for SignalBus events
3
- *
4
- * Extends the base Collector to capture all signals emitted through the SignalBus.
5
- * Uses wildcard subscription to capture all domain:action events.
6
- *
7
- * @example
8
- * const collector = new SignalCollector({ maxEntries: 50 })
9
- * collector.install(ctx) // ctx.signals is the SignalBus
10
- *
11
- * // Later, retrieve captured signals
12
- * collector.getEntries() // [{ name, data, source, timestamp }, ...]
2
+ * SignalCollector qdadm re-export from @quazardous/qddebug.
13
3
  */
14
-
15
- import { Collector, type CollectorContext, type CollectorEntry } from './Collector'
16
-
17
- /**
18
- * Signal entry type
19
- */
20
- export interface SignalEntry extends CollectorEntry {
21
- name: string
22
- data: unknown
23
- source: string | null
24
- }
25
-
26
- /**
27
- * Collector for SignalBus events
28
- *
29
- * Records all signals with their name, data, and source for debugging.
30
- * Uses the `**` wildcard pattern to capture all signals including
31
- * multi-segment names (e.g., entity:data-invalidate, auth:impersonate:start).
32
- */
33
- export class SignalCollector extends Collector<SignalEntry> {
34
- /**
35
- * Collector name for identification
36
- */
37
- static override collectorName = 'signals'
38
-
39
- /**
40
- * Signals to skip recording (internal kernel signals with non-serializable data)
41
- * @private
42
- */
43
- private static _skipSignals = new Set(['kernel:ready', 'kernel:shutdown'])
44
-
45
- private _unsubscribe: (() => void) | null = null
46
-
47
- /**
48
- * Internal install - subscribe to all signals
49
- * @protected
50
- */
51
- protected override _doInstall(ctx: CollectorContext): void {
52
- if (!ctx?.signals) {
53
- console.warn('[SignalCollector] No signals bus found in context')
54
- return
55
- }
56
-
57
- // Subscribe to all signals using wildcard pattern
58
- // QuarKernel supports wildcards with the configured delimiter (:)
59
- // '**' matches all signals including multi-segment (entity:data-invalidate)
60
- this._unsubscribe = ctx.signals.on('**', (event) => {
61
- // Skip internal signals with non-serializable data (kernel, orchestrator)
62
- if (SignalCollector._skipSignals.has(event.name)) {
63
- return
64
- }
65
-
66
- // Sanitize data to avoid cyclic references
67
- const data = this._sanitizeData(event.data)
68
-
69
- this.record({
70
- name: event.name,
71
- data,
72
- source: (data as Record<string, unknown>)?.source as string ?? null
73
- })
74
- })
75
- }
76
-
77
- /**
78
- * Sanitize event data to remove non-serializable objects
79
- *
80
- * Handles cases where data contains references to Kernel, Orchestrator,
81
- * or other complex objects that would cause cyclic reference errors.
82
- *
83
- * @param data - Raw event data
84
- * @returns Sanitized data safe for recording
85
- * @private
86
- */
87
- private _sanitizeData(data: unknown): unknown {
88
- if (data === null || data === undefined) return data
89
- if (typeof data !== 'object') return data
90
-
91
- // Try to create a simple clone, fallback to description if cyclic
92
- try {
93
- const obj = data as Record<string, unknown>
94
- // Quick check for obviously problematic properties
95
- if (obj.kernel || obj.orchestrator || obj._kernel) {
96
- // Extract only safe properties
97
- const safe: Record<string, unknown> = {}
98
- for (const [key, value] of Object.entries(obj)) {
99
- if (key !== 'kernel' && key !== 'orchestrator' && key !== '_kernel') {
100
- if (typeof value !== 'object' || value === null) {
101
- safe[key] = value
102
- } else if (Array.isArray(value)) {
103
- safe[key] = `[Array(${value.length})]`
104
- } else {
105
- safe[key] = '[Object]'
106
- }
107
- }
108
- }
109
- return safe
110
- }
111
-
112
- // For other objects, try JSON roundtrip to detect cycles
113
- JSON.stringify(data)
114
- return data
115
- } catch {
116
- // If serialization fails, return a safe representation
117
- return { _type: 'unserializable', keys: Object.keys(data as object) }
118
- }
119
- }
120
-
121
- /**
122
- * Internal uninstall - cleanup subscription
123
- * @protected
124
- */
125
- protected override _doUninstall(): void {
126
- if (this._unsubscribe) {
127
- this._unsubscribe()
128
- this._unsubscribe = null
129
- }
130
- }
131
-
132
- /**
133
- * Get entries filtered by signal name pattern
134
- *
135
- * @param pattern - Pattern to match signal names
136
- * @returns Filtered entries
137
- */
138
- getByPattern(pattern: string | RegExp): SignalEntry[] {
139
- const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern)
140
- return this.entries.filter((entry) => regex.test(entry.name))
141
- }
142
-
143
- /**
144
- * Get entries for a specific signal domain
145
- *
146
- * @param domain - Domain prefix (e.g., 'entity', 'auth', 'books')
147
- * @returns Entries matching the domain
148
- */
149
- getByDomain(domain: string): SignalEntry[] {
150
- return this.entries.filter((entry) => entry.name.startsWith(`${domain}:`))
151
- }
152
- }
4
+ export { SignalCollector, type SignalEntry } from '@quazardous/qddebug'
@@ -1,93 +1,4 @@
1
1
  /**
2
- * ToastCollector - Captures toast notifications via signal bus
3
- *
4
- * This collector listens to toast signals emitted on the signal bus
5
- * and records them for display in the debug panel.
6
- *
7
- * Toast signals should follow the pattern:
8
- * - signals.emit('toast:success', { summary: '...', detail: '...' })
9
- * - signals.emit('toast:error', { summary: '...', detail: '...' })
10
- * - signals.emit('toast:info', { summary: '...', detail: '...' })
11
- * - signals.emit('toast:warn', { summary: '...', detail: '...' })
12
- *
13
- * Use with ToastBridge module which handles displaying toasts via PrimeVue.
14
- *
15
- * @example
16
- * const collector = new ToastCollector()
17
- * collector.install(ctx)
18
- * // Toasts emitted via signals are now automatically recorded
2
+ * ToastCollector qdadm re-export from @quazardous/qddebug.
19
3
  */
20
-
21
- import { Collector, type CollectorContext, type CollectorEntry } from './Collector'
22
-
23
- /**
24
- * Toast entry type
25
- */
26
- export interface ToastEntry extends CollectorEntry {
27
- severity: string
28
- summary?: string
29
- detail?: string
30
- life?: number
31
- emitter: string
32
- }
33
-
34
- /**
35
- * Collector for toast notifications
36
- */
37
- export class ToastCollector extends Collector<ToastEntry> {
38
- /**
39
- * Collector name identifier
40
- */
41
- static override collectorName = 'toasts'
42
-
43
- private _unsubscribe: (() => void) | null = null
44
-
45
- /**
46
- * Internal install - subscribe to toast signals
47
- * @protected
48
- */
49
- protected override _doInstall(ctx: CollectorContext): void {
50
- // Listen for toast signals on the signal bus
51
- if (ctx?.signals) {
52
- this._unsubscribe = ctx.signals.on('toast:*', (event) => {
53
- const data = event.data as { summary?: string; detail?: string; life?: number; emitter?: string } | undefined
54
- this.record({
55
- severity: event.name.split(':')[1] ?? 'info', // toast:success -> success
56
- summary: data?.summary,
57
- detail: data?.detail,
58
- life: data?.life,
59
- emitter: data?.emitter ?? 'unknown'
60
- })
61
- })
62
- } else {
63
- console.warn('[ToastCollector] No signals bus in context - toast recording disabled')
64
- }
65
- }
66
-
67
- /**
68
- * Internal uninstall - cleanup
69
- * @protected
70
- */
71
- protected override _doUninstall(): void {
72
- if (this._unsubscribe) {
73
- this._unsubscribe()
74
- this._unsubscribe = null
75
- }
76
- }
77
-
78
- /**
79
- * Get entries by severity
80
- * @param severity - Severity level (success, info, warn, error)
81
- * @returns Filtered entries
82
- */
83
- getBySeverity(severity: string): ToastEntry[] {
84
- return this.entries.filter((entry) => entry.severity === severity)
85
- }
86
-
87
- /**
88
- * Get error toasts count for badge
89
- */
90
- getErrorCount(): number {
91
- return this.entries.filter((e) => e.severity === 'error').length
92
- }
93
- }
4
+ export { ToastCollector, type ToastEntry } from '@quazardous/qddebug'
@@ -15,7 +15,14 @@
15
15
  * collector.getZoneInfo() // { zones: [...], totalBlocks: n }
16
16
  */
17
17
 
18
- import { Collector, type CollectorContext, type CollectorEntry, type CollectorOptions } from './Collector'
18
+ import {
19
+ Collector,
20
+ type CollectorContext,
21
+ type CollectorEntry,
22
+ type CollectorManifest,
23
+ type CollectorOptions,
24
+ type CollectorSnapshot,
25
+ } from './Collector'
19
26
  import type { ZoneRegistry } from '../../zones/ZoneRegistry'
20
27
  import type { Router } from 'vue-router'
21
28
 
@@ -352,4 +359,89 @@ export class ZonesCollector extends Collector<ZoneEntry> {
352
359
  return true
353
360
  }
354
361
  }
362
+
363
+ override describe(): CollectorManifest {
364
+ return {
365
+ name: this.name,
366
+ records: false,
367
+ summary:
368
+ 'Lists every zone defined in the running app and the blocks/wrappers registered in each. Reflects current page (filterable).',
369
+ entryShape: {
370
+ name: 'string',
371
+ isOnPage: 'boolean',
372
+ hasDefault: 'boolean',
373
+ defaultName: 'string?',
374
+ blocksCount: 'number',
375
+ blocks: 'ZoneBlock[]',
376
+ },
377
+ stateShape: {
378
+ showCurrentPageOnly: 'boolean',
379
+ showInternalZones: 'boolean',
380
+ highlightedZone: 'string?',
381
+ },
382
+ actions: [
383
+ ...this._builtinActionManifests(),
384
+ {
385
+ name: 'highlight',
386
+ summary: 'Draw an overlay on the page around a zone (browser-only).',
387
+ args: { name: 'string' },
388
+ mutates: true,
389
+ },
390
+ {
391
+ name: 'clearHighlights',
392
+ summary: 'Remove every zone overlay.',
393
+ mutates: true,
394
+ },
395
+ {
396
+ name: 'setShowCurrentPageOnly',
397
+ summary: 'Filter zones to those rendered on the current page.',
398
+ args: { value: 'boolean' },
399
+ mutates: true,
400
+ },
401
+ {
402
+ name: 'setShowInternalZones',
403
+ summary: 'Show internal zones (prefixed with _).',
404
+ args: { value: 'boolean' },
405
+ mutates: true,
406
+ },
407
+ ],
408
+ }
409
+ }
410
+
411
+ override snapshot(): CollectorSnapshot {
412
+ const entries = this.getEntries(true)
413
+ return {
414
+ name: this.name,
415
+ entries,
416
+ count: entries.length,
417
+ unseen: 0,
418
+ state: {
419
+ showCurrentPageOnly: this._showCurrentPageOnly,
420
+ showInternalZones: this._showInternalZones,
421
+ highlightedZone: this._highlightedZone,
422
+ },
423
+ }
424
+ }
425
+
426
+ override async call(actionName: string, args: Record<string, unknown> = {}): Promise<unknown> {
427
+ if (actionName === 'highlight') {
428
+ this.highlightZone(String(args.name ?? ''))
429
+ return { ok: true }
430
+ }
431
+ if (actionName === 'clearHighlights') {
432
+ this.clearHighlights()
433
+ return { ok: true }
434
+ }
435
+ if (actionName === 'setShowCurrentPageOnly') {
436
+ this._showCurrentPageOnly = Boolean(args.value)
437
+ this.notifyChange()
438
+ return { ok: true, value: this._showCurrentPageOnly }
439
+ }
440
+ if (actionName === 'setShowInternalZones') {
441
+ this._showInternalZones = Boolean(args.value)
442
+ this.notifyChange()
443
+ return { ok: true, value: this._showInternalZones }
444
+ }
445
+ return super.call(actionName, args)
446
+ }
355
447
  }