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
@@ -1,437 +1,16 @@
1
1
  /**
2
- * HookRegistry - Drupal-inspired hook system for qdadm
2
+ * HookRegistry qdadm re-export.
3
3
  *
4
- * Provides a hook-based extension API built on QuarKernel's signal bus.
5
- * Two hook types:
6
- * - Lifecycle hooks: fire-and-forget, no return value expected (via invoke())
7
- * - Alter hooks: chained transforms, each handler receives previous output (via alter())
8
- *
9
- * Hook naming convention: colon-delimited (e.g., 'entity:presave', 'list:alter')
10
- *
11
- * Handler signature: (event, listenerContext) => void | Promise<void>
12
- * - event.data contains the context/data passed to invoke()/alter()
13
- * - event.name is the hook name
14
- * - listenerContext provides utilities (id, cancel, emit, etc.)
15
- *
16
- * @example
17
- * // Register lifecycle hook (fire-and-forget)
18
- * // Handlers receive QuarKernel event - context is in event.data
19
- * hooks.register('entity:presave', async (event) => {
20
- * event.data.entity.updated_at = Date.now()
21
- * }, { priority: 10 })
22
- *
23
- * // Register alter hook (transform chain)
24
- * hooks.register('list:alter', (config) => {
25
- * config.columns.push({ field: 'custom' })
26
- * return config
27
- * })
28
- *
29
- * // Invoke lifecycle hook
30
- * await hooks.invoke('entity:presave', { entity, manager })
31
- *
32
- * // Invoke alter hook
33
- * const alteredConfig = await hooks.alter('list:alter', baseConfig)
34
- */
35
-
36
- import { createKernel, type QuarKernel, type ListenerOptions } from '@quazardous/quarkernel'
37
-
38
- /**
39
- * Default priority for hooks (middle of range)
40
- */
41
- const DEFAULT_PRIORITY = 50
42
-
43
- /**
44
- * Priority constants for common use cases
45
- */
46
- export const HOOK_PRIORITY = {
47
- FIRST: 100,
48
- HIGH: 75,
49
- NORMAL: 50,
50
- LOW: 25,
51
- LAST: 0,
52
- } as const
53
-
54
- /**
55
- * Hook handler function type
56
- */
57
- export type HookHandler<T = unknown> = (data: T) => T | void | Promise<T | void>
58
-
59
- /**
60
- * Hook registration options
61
- */
62
- export interface HookRegistrationOptions {
63
- priority?: number
64
- id?: string
65
- after?: string | string[]
66
- once?: boolean
67
- }
68
-
69
- /**
70
- * Hook entry for internal tracking
71
- */
72
- interface HookEntry {
73
- handler: HookHandler
74
- priority: number
75
- id?: string
76
- after?: string | string[]
77
- once: boolean
78
- unbind: () => void
79
- }
80
-
81
- /**
82
- * Hook entry with normalized dependencies
83
- */
84
- interface HookEntryWithDeps extends HookEntry {
85
- afterArray: string[]
86
- }
87
-
88
- /**
89
- * HookRegistry constructor options
90
- */
91
- export interface HookRegistryOptions {
92
- kernel?: QuarKernel
93
- debug?: boolean
94
- }
95
-
96
- /**
97
- * Invoke options
98
- */
99
- export interface InvokeOptions {
100
- throwOnError?: boolean
101
- }
102
-
103
- /**
104
- * Alter options
105
- */
106
- export interface AlterOptions {
107
- immutable?: boolean
108
- }
109
-
110
- /**
111
- * HookRegistry class - wraps QuarKernel for Drupal-inspired hook API
112
- */
113
- export class HookRegistry {
114
- private _kernel: QuarKernel
115
- private _hooks: Map<string, HookEntry[]>
116
-
117
- /**
118
- * Create a HookRegistry instance
119
- *
120
- * @param options - Configuration options
121
- */
122
- constructor(options: HookRegistryOptions = {}) {
123
- this._kernel =
124
- options.kernel ??
125
- createKernel({
126
- delimiter: ':',
127
- wildcard: true,
128
- errorBoundary: true,
129
- debug: options.debug ?? false,
130
- })
131
-
132
- // Track registered hooks for introspection
133
- this._hooks = new Map()
134
- }
135
-
136
- /**
137
- * Register a hook handler
138
- *
139
- * @param name - Hook name (colon-delimited, e.g., 'entity:presave')
140
- * @param handler - Handler function
141
- * @param options - Registration options
142
- * @returns Unbind function to remove this handler
143
- */
144
- register(name: string, handler: HookHandler, options: HookRegistrationOptions = {}): () => void {
145
- const { priority = DEFAULT_PRIORITY, id, after, once = false } = options
146
-
147
- // Build QuarKernel options
148
- const kernelOptions: ListenerOptions = {
149
- priority,
150
- once,
151
- }
152
-
153
- if (id) {
154
- kernelOptions.id = id
155
- }
156
-
157
- if (after) {
158
- kernelOptions.after = after
159
- }
160
-
161
- // Register with QuarKernel
162
- const unbind = this._kernel.on(name, handler as never, kernelOptions)
163
-
164
- // Track for introspection
165
- if (!this._hooks.has(name)) {
166
- this._hooks.set(name, [])
167
- }
168
-
169
- const hookEntry: HookEntry = {
170
- handler,
171
- priority,
172
- id,
173
- after,
174
- once,
175
- unbind,
176
- }
177
-
178
- this._hooks.get(name)!.push(hookEntry)
179
-
180
- // Return enhanced unbind that also cleans up tracking
181
- return () => {
182
- unbind()
183
- const hooks = this._hooks.get(name)
184
- if (hooks) {
185
- const idx = hooks.indexOf(hookEntry)
186
- if (idx !== -1) {
187
- hooks.splice(idx, 1)
188
- }
189
- if (hooks.length === 0) {
190
- this._hooks.delete(name)
191
- }
192
- }
193
- }
194
- }
195
-
196
- /**
197
- * Invoke a lifecycle hook (fire-and-forget)
198
- *
199
- * All registered handlers are called with the context in priority/dependency order.
200
- * Handlers are executed sequentially (serial) to ensure predictable mutation order.
201
- * Handlers may be async; all are awaited before returning.
202
- * No return value is expected from handlers.
203
- *
204
- * @param name - Hook name
205
- * @param context - Context object passed to all handlers (can be mutated)
206
- * @param options - Invocation options
207
- * @throws AggregateError if throwOnError is true and any handlers threw errors
208
- */
209
- async invoke(name: string, context: unknown = {}, options: InvokeOptions = {}): Promise<void> {
210
- const { throwOnError = false } = options
211
-
212
- // Clear any previous execution errors
213
- this._kernel.clearExecutionErrors()
214
-
215
- // Use emitSerial for guaranteed sequential execution (predictable mutation order)
216
- // QuarKernel's errorBoundary: true continues on errors, collecting them
217
- await this._kernel.emitSerial(name, context)
218
-
219
- // If requested, rethrow collected errors after all handlers have run
220
- if (throwOnError) {
221
- const errors = this._kernel.getExecutionErrors()
222
- if (errors.length > 0) {
223
- const errorMessages = errors.map(
224
- (e: { listenerId: string; error: Error }) => `[${e.listenerId}] ${e.error.message}`
225
- )
226
- throw new AggregateError(
227
- errors.map((e: { error: Error }) => e.error),
228
- `Hook "${name}" handlers failed:\n ${errorMessages.join('\n ')}`
229
- )
230
- }
231
- }
232
- }
233
-
234
- /**
235
- * Invoke an alter hook (chained transforms)
236
- *
237
- * Each handler receives the current data and returns the transformed data.
238
- * Handlers are chained: each receives the output of the previous handler (reduce pattern).
239
- * Respects priority ordering (higher priority runs first) and `after` dependencies.
240
- *
241
- * @param name - Hook name
242
- * @param data - Initial data to transform
243
- * @param options - Alter options
244
- * @returns Transformed data after all handlers
245
- *
246
- * @example
247
- * // Register handlers
248
- * hooks.register('list:alter', (config) => {
249
- * config.columns.push({ field: 'custom' })
250
- * return config // MUST return
251
- * }, { priority: HOOK_PRIORITY.NORMAL })
252
- *
253
- * // Invoke alter hook
254
- * const config = await hooks.alter('list:alter', baseConfig)
255
- *
256
- * // With immutability (each handler gets a fresh clone)
257
- * const config = await hooks.alter('list:alter', baseConfig, { immutable: true })
258
- */
259
- async alter<T>(name: string, data: T, options: AlterOptions = {}): Promise<T> {
260
- const { immutable = false } = options
261
-
262
- // No handlers? Return data unchanged
263
- if (!this.hasHook(name)) {
264
- return data
265
- }
266
-
267
- // Get our tracked handlers (they have priority and dependency info)
268
- const handlers = this._hooks.get(name) || []
269
-
270
- // Sort by dependencies and priority using the same algorithm as QuarKernel
271
- const sortedHandlers = this._sortByDependencies(handlers)
272
-
273
- // Chain through handlers: each receives previous output (reduce pattern)
274
- let result: T = data
275
- for (const entry of sortedHandlers) {
276
- // Clone data if immutability is enabled
277
- const input = immutable ? this._cloneData(result) : result
278
- const handlerResult = await entry.handler(input)
279
-
280
- // If handler returns something, use it; otherwise keep current
281
- if (handlerResult !== undefined) {
282
- result = handlerResult as T
283
- }
284
- }
285
-
286
- return result
287
- }
288
-
289
- /**
290
- * Clone data for immutability support
291
- */
292
- private _cloneData<T>(val: T): T {
293
- if (val === null || val === undefined) {
294
- return val
295
- }
296
- if (typeof val !== 'object') {
297
- return val
298
- }
299
- // Use structuredClone for deep copy (available in Node 17+)
300
- return structuredClone(val)
301
- }
302
-
303
- /**
304
- * Sort handlers by dependencies and priority
305
- * Uses topological sort for dependency resolution (after option)
306
- */
307
- private _sortByDependencies(handlers: HookEntry[]): HookEntryWithDeps[] {
308
- // If no dependencies, just return sorted by priority
309
- const hasDependencies = handlers.some(
310
- (h) => h.after && (typeof h.after === 'string' || (Array.isArray(h.after) && h.after.length > 0))
311
- )
312
-
313
- const entriesWithDeps: HookEntryWithDeps[] = handlers.map((h) => ({
314
- ...h,
315
- afterArray: h.after ? (Array.isArray(h.after) ? h.after : [h.after]) : [],
316
- }))
317
-
318
- if (!hasDependencies) {
319
- return entriesWithDeps.sort((a, b) => b.priority - a.priority)
320
- }
321
-
322
- // Build ID set for validation
323
- const handlerIds = new Set(entriesWithDeps.filter((e) => e.id).map((e) => e.id))
324
-
325
- // Filter dependencies to only those that exist in this hook
326
- for (const entry of entriesWithDeps) {
327
- entry.afterArray = entry.afterArray.filter((dep) => handlerIds.has(dep))
328
- }
329
-
330
- // Assign dependency levels
331
- const levelMap = new Map<HookEntryWithDeps, number>()
332
- const assignLevel = (entry: HookEntryWithDeps, visited: Set<HookEntryWithDeps> = new Set()): number => {
333
- if (levelMap.has(entry)) {
334
- return levelMap.get(entry)!
335
- }
336
- if (visited.has(entry)) {
337
- // Circular dependency - treat as level 0
338
- return 0
339
- }
340
- visited.add(entry)
341
-
342
- if (entry.afterArray.length === 0) {
343
- levelMap.set(entry, 0)
344
- return 0
345
- }
346
-
347
- // Find max level of dependencies
348
- let maxDepLevel = 0
349
- for (const depId of entry.afterArray) {
350
- const depEntry = entriesWithDeps.find((e) => e.id === depId)
351
- if (depEntry) {
352
- const depLevel = assignLevel(depEntry, new Set(visited))
353
- maxDepLevel = Math.max(maxDepLevel, depLevel)
354
- }
355
- }
356
-
357
- const level = maxDepLevel + 1
358
- levelMap.set(entry, level)
359
- return level
360
- }
361
-
362
- // Assign levels to all entries
363
- entriesWithDeps.forEach((e) => assignLevel(e))
364
-
365
- // Sort by level, then by priority within level
366
- return [...entriesWithDeps].sort((a, b) => {
367
- const levelA = levelMap.get(a) ?? 0
368
- const levelB = levelMap.get(b) ?? 0
369
- if (levelA !== levelB) {
370
- return levelA - levelB // Lower level first (dependencies run first)
371
- }
372
- return b.priority - a.priority // Higher priority first within same level
373
- })
374
- }
375
-
376
- /**
377
- * Get all registered hook names
378
- */
379
- getRegisteredHooks(): string[] {
380
- return Array.from(this._hooks.keys())
381
- }
382
-
383
- /**
384
- * Get handler count for a hook
385
- *
386
- * @param name - Hook name (optional, total if omitted)
387
- */
388
- getHandlerCount(name?: string): number {
389
- if (name) {
390
- return this._hooks.get(name)?.length ?? 0
391
- }
392
-
393
- let total = 0
394
- for (const handlers of this._hooks.values()) {
395
- total += handlers.length
396
- }
397
- return total
398
- }
399
-
400
- /**
401
- * Check if a hook has any registered handlers
402
- */
403
- hasHook(name: string): boolean {
404
- return this._hooks.has(name) && this._hooks.get(name)!.length > 0
405
- }
406
-
407
- /**
408
- * Clean up all registered hooks
409
- *
410
- * Removes all handlers and clears internal state.
411
- */
412
- dispose(): void {
413
- // Unbind all handlers
414
- for (const handlers of this._hooks.values()) {
415
- for (const entry of handlers) {
416
- entry.unbind()
417
- }
418
- }
419
-
420
- // Clear tracking
421
- this._hooks.clear()
422
- }
423
-
424
- /**
425
- * Enable/disable debug mode
426
- */
427
- debug(enabled: boolean): void {
428
- this._kernel.debug(enabled)
429
- }
430
- }
431
-
432
- /**
433
- * Factory function to create a HookRegistry instance
434
- */
435
- export function createHookRegistry(options: HookRegistryOptions = {}): HookRegistry {
436
- return new HookRegistry(options)
437
- }
4
+ * The actual implementation now lives in `@quazardous/qdcore` so it can be
5
+ * shared with qdcms. This module re-exports the public API to preserve
6
+ * existing qdadm imports (`from '../hooks/HookRegistry'`).
7
+ */
8
+
9
+ export { HookRegistry, createHookRegistry, HOOK_PRIORITY } from '@quazardous/qdcore'
10
+ export type {
11
+ HookHandler,
12
+ HookRegistrationOptions,
13
+ HookRegistryOptions,
14
+ InvokeOptions,
15
+ AlterOptions,
16
+ } from '@quazardous/qdcore'