qdadm 0.53.2 → 0.55.0

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 (42) hide show
  1. package/package.json +2 -2
  2. package/src/chain/ActiveStack.js +133 -37
  3. package/src/chain/StackHydrator.js +279 -0
  4. package/src/chain/index.js +9 -2
  5. package/src/chain/useActiveStack.js +55 -98
  6. package/src/chain/useStackHydrator.js +105 -0
  7. package/src/composables/useBreadcrumb.js +15 -2
  8. package/src/composables/useCurrentEntity.js +11 -11
  9. package/src/composables/useEntityItemFormPage.js +5 -5
  10. package/src/composables/useEntityItemPage.js +7 -7
  11. package/src/composables/useForm.js +3 -3
  12. package/src/composables/useNavContext.js +16 -18
  13. package/src/composables/useSemanticBreadcrumb.js +3 -3
  14. package/src/kernel/Kernel.js +99 -6
  15. package/src/kernel/KernelContext.js +16 -0
  16. package/src/kernel/Module.js +32 -0
  17. package/src/kernel/ModuleLoader.js +5 -1
  18. package/src/{debug → modules/debug}/DebugModule.js +7 -1
  19. package/src/{debug → modules/debug}/RouterCollector.js +53 -2
  20. package/src/{debug → modules/debug}/components/DebugBar.vue +0 -528
  21. package/src/{debug → modules/debug}/components/ObjectTree.vue +0 -80
  22. package/src/{debug → modules/debug}/components/panels/AuthPanel.vue +0 -100
  23. package/src/{debug → modules/debug}/components/panels/EntitiesPanel.vue +0 -524
  24. package/src/{debug → modules/debug}/components/panels/EntriesPanel.vue +0 -117
  25. package/src/{debug → modules/debug}/components/panels/RouterPanel.vue +120 -420
  26. package/src/{debug → modules/debug}/components/panels/SignalsPanel.vue +0 -164
  27. package/src/modules/debug/components/panels/ToastsPanel.vue +34 -0
  28. package/src/{debug → modules/debug}/components/panels/ZonesPanel.vue +0 -131
  29. package/src/modules/debug/styles.scss +2469 -0
  30. package/src/debug/components/panels/ToastsPanel.vue +0 -112
  31. /package/src/{debug → modules/debug}/AuthCollector.js +0 -0
  32. /package/src/{debug → modules/debug}/Collector.js +0 -0
  33. /package/src/{debug → modules/debug}/DebugBridge.js +0 -0
  34. /package/src/{debug → modules/debug}/EntitiesCollector.js +0 -0
  35. /package/src/{debug → modules/debug}/ErrorCollector.js +0 -0
  36. /package/src/{debug → modules/debug}/LocalStorageAdapter.js +0 -0
  37. /package/src/{debug → modules/debug}/SignalCollector.js +0 -0
  38. /package/src/{debug → modules/debug}/ToastCollector.js +0 -0
  39. /package/src/{debug → modules/debug}/ZonesCollector.js +0 -0
  40. /package/src/{debug → modules/debug}/components/index.js +0 -0
  41. /package/src/{debug → modules/debug}/components/panels/index.js +0 -0
  42. /package/src/{debug → modules/debug}/index.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.53.2",
3
+ "version": "0.55.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "./editors": "./src/editors/index.js",
28
28
  "./module": "./src/module/index.js",
29
29
  "./utils": "./src/utils/index.js",
30
- "./debug": "./src/debug/index.js",
30
+ "./modules/debug": "./src/modules/debug/index.js",
31
31
  "./styles": "./src/styles/index.scss",
32
32
  "./styles/breakpoints": "./src/styles/_breakpoints.scss",
33
33
  "./gen": "./src/gen/index.js",
@@ -1,74 +1,170 @@
1
1
  /**
2
- * ActiveStack - Reactive container for the active navigation stack
2
+ * ActiveStack - Sync navigation context from route
3
3
  *
4
- * Simple container rebuilt from route by useActiveStack.
5
- * Each level: { entity, id, data, label }
4
+ * Pure vanilla JS container using SignalBus for events.
5
+ * Rebuilt from route.meta on navigation.
6
+ *
7
+ * Stack only contains levels WITH IDs (entities with context).
8
+ * - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
9
+ * - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
10
+ *
11
+ * Signals emitted:
12
+ * - stack:change - when stack levels change
13
+ *
14
+ * For Vue reactivity, use useActiveStack composable.
6
15
  *
7
16
  * @example
8
- * // Route /books/123/loans Stack:
9
- * [
10
- * { entity: 'books', id: '123', data: null, label: 'Books' },
11
- * { entity: 'loans', id: null, data: null, label: 'Loans' }
12
- * ]
17
+ * const stack = new ActiveStack(signalBus)
18
+ * signalBus.on('stack:change', ({ levels }) => console.log('Stack:', levels))
19
+ * stack.set([{ entity: 'bots', param: 'uuid', id: 'bot-123' }])
13
20
  */
14
21
 
15
- import { ref, computed } from 'vue'
22
+ /**
23
+ * @typedef {Object} StackLevel
24
+ * @property {string} entity - Entity name (e.g., 'bots', 'commands')
25
+ * @property {string} param - Route param name (e.g., 'uuid', 'id')
26
+ * @property {string|null} foreignKey - Foreign key field for parent relation (null for root)
27
+ * @property {string|null} id - Entity ID from route params
28
+ */
16
29
 
17
30
  export class ActiveStack {
18
- constructor() {
19
- this._stack = ref([])
31
+ /**
32
+ * @param {import('../kernel/SignalBus.js').SignalBus} [signalBus] - Optional signal bus for events
33
+ */
34
+ constructor(signalBus = null) {
35
+ /** @type {StackLevel[]} */
36
+ this._levels = []
37
+
38
+ /** @type {import('../kernel/SignalBus.js').SignalBus|null} */
39
+ this._signalBus = signalBus
20
40
  }
21
41
 
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+ // Mutators
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+
22
46
  /**
23
47
  * Replace entire stack (called on route change)
48
+ * Only emits if levels actually changed (prevents duplicate emissions)
49
+ * @param {StackLevel[]} levels
50
+ *
51
+ * BUG: Still getting duplicate signals (4 instead of 2) on navigation.
52
+ * Equality check should prevent this but something is triggering multiple calls
53
+ * with different levels. Need to investigate router.afterEach timing.
24
54
  */
25
55
  set(levels) {
26
- this._stack.value = levels
56
+ // Quick equality check - same length and same entity+id pairs
57
+ if (this._levelsEqual(levels)) {
58
+ return
59
+ }
60
+ this._levels = levels
61
+ this._emit('stack:change', { levels: this._levels })
27
62
  }
28
63
 
29
64
  /**
30
- * Update a level's data and label
65
+ * Check if new levels match current levels
66
+ * @param {StackLevel[]} newLevels
67
+ * @returns {boolean}
68
+ * @private
31
69
  */
32
- updateLevel(index, data, label) {
33
- if (index < 0 || index >= this._stack.value.length) return
34
- const newStack = [...this._stack.value]
35
- newStack[index] = { ...newStack[index], data, label }
36
- this._stack.value = newStack
70
+ _levelsEqual(newLevels) {
71
+ if (this._levels.length !== newLevels.length) return false
72
+ for (let i = 0; i < this._levels.length; i++) {
73
+ const curr = this._levels[i]
74
+ const next = newLevels[i]
75
+ if (curr.entity !== next.entity || curr.id !== next.id) {
76
+ return false
77
+ }
78
+ }
79
+ return true
37
80
  }
38
81
 
39
82
  /**
40
- * Find and update level by entity name
83
+ * Clear the stack
84
+ * Only emits if not already empty
41
85
  */
42
- updateByEntity(entity, data, label) {
43
- const index = this._stack.value.findIndex(l => l.entity === entity)
44
- if (index !== -1) {
45
- this.updateLevel(index, data, label)
46
- }
86
+ clear() {
87
+ if (this._levels.length === 0) return
88
+ this._levels = []
89
+ this._emit('stack:change', { levels: this._levels })
47
90
  }
48
91
 
49
- clear() {
50
- this._stack.value = []
92
+ // ═══════════════════════════════════════════════════════════════════════════
93
+ // Accessors
94
+ // ═══════════════════════════════════════════════════════════════════════════
95
+
96
+ /**
97
+ * All stack levels
98
+ * @returns {StackLevel[]}
99
+ */
100
+ getLevels() {
101
+ return this._levels
51
102
  }
52
103
 
53
- // Computed accessors
54
- get levels() {
55
- return computed(() => this._stack.value)
104
+ /**
105
+ * Get level by index
106
+ * @param {number} index
107
+ * @returns {StackLevel|null}
108
+ */
109
+ getLevel(index) {
110
+ return this._levels[index] ?? null
56
111
  }
57
112
 
58
- get current() {
59
- return computed(() => this._stack.value.at(-1) || null)
113
+ /**
114
+ * Get level by entity name
115
+ * @param {string} entity
116
+ * @returns {StackLevel|null}
117
+ */
118
+ getLevelByEntity(entity) {
119
+ return this._levels.find(l => l.entity === entity) ?? null
120
+ }
121
+
122
+ /**
123
+ * Current (deepest) level
124
+ * @returns {StackLevel|null}
125
+ */
126
+ getCurrent() {
127
+ return this._levels.at(-1) ?? null
128
+ }
129
+
130
+ /**
131
+ * Parent level (one above current)
132
+ * @returns {StackLevel|null}
133
+ */
134
+ getParent() {
135
+ return this._levels.at(-2) ?? null
60
136
  }
61
137
 
62
- get parent() {
63
- return computed(() => this._stack.value.at(-2) || null)
138
+ /**
139
+ * Root level (first/topmost)
140
+ * @returns {StackLevel|null}
141
+ */
142
+ getRoot() {
143
+ return this._levels[0] ?? null
64
144
  }
65
145
 
66
- get root() {
67
- return computed(() => this._stack.value[0] || null)
146
+ /**
147
+ * Stack depth
148
+ * @returns {number}
149
+ */
150
+ getDepth() {
151
+ return this._levels.length
68
152
  }
69
153
 
70
- get depth() {
71
- return computed(() => this._stack.value.length)
154
+ // ═══════════════════════════════════════════════════════════════════════════
155
+ // Internal
156
+ // ═══════════════════════════════════════════════════════════════════════════
157
+
158
+ /**
159
+ * Emit event via SignalBus
160
+ * @param {string} signal
161
+ * @param {*} payload
162
+ * @private
163
+ */
164
+ _emit(signal, payload) {
165
+ if (this._signalBus) {
166
+ this._signalBus.emit(signal, payload)
167
+ }
72
168
  }
73
169
  }
74
170
 
@@ -0,0 +1,279 @@
1
+ /**
2
+ * StackHydrator - Async hydration layer for ActiveStack
3
+ *
4
+ * Pure vanilla JS using SignalBus for events.
5
+ * Listens to stack:change and hydrates each level with entity data.
6
+ *
7
+ * Signals emitted:
8
+ * - stack:hydration:change - when a level's hydration completes { levels }
9
+ *
10
+ * For Vue reactivity, use useStackHydrator composable.
11
+ *
12
+ * @example
13
+ * const hydrator = new StackHydrator(activeStack, orchestrator, signalBus)
14
+ * signalBus.on('stack:hydration:change', (event) => console.log('Hydrated:', event.data.levels))
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} HydratedLevel
19
+ * @property {string} entity - Entity name
20
+ * @property {string} param - Route param name
21
+ * @property {string|null} foreignKey - Foreign key field
22
+ * @property {string|null} id - Entity ID
23
+ * @property {Promise<void>} promise - Resolves when hydrated
24
+ * @property {boolean} loading - True while fetching
25
+ * @property {boolean} hydrated - True when data is loaded
26
+ * @property {Error|null} error - Error if fetch failed
27
+ * @property {Record<string, any>|null} data - Entity data
28
+ * @property {string|null} label - Resolved label
29
+ */
30
+
31
+ export class StackHydrator {
32
+ /**
33
+ * @param {import('./ActiveStack.js').ActiveStack} activeStack
34
+ * @param {import('../orchestrator/Orchestrator.js').Orchestrator} orchestrator
35
+ * @param {import('../kernel/SignalBus.js').SignalBus} [signalBus]
36
+ */
37
+ constructor(activeStack, orchestrator, signalBus = null) {
38
+ this._activeStack = activeStack
39
+ this._orchestrator = orchestrator
40
+ this._signalBus = signalBus
41
+
42
+ /** @type {HydratedLevel[]} */
43
+ this._levels = []
44
+
45
+ // Listen to stack changes via SignalBus
46
+ // QuarKernel passes (event, ctx) where event.data contains the payload
47
+ if (signalBus) {
48
+ this._unsubscribe = signalBus.on('stack:change', (event) => {
49
+ const levels = event.data?.levels
50
+ if (levels) {
51
+ this._onStackChange(levels)
52
+ }
53
+ })
54
+ }
55
+ // Note: No initial check needed - stack is always empty at construction time
56
+ // The Kernel's router.afterEach will trigger stack:change after navigation
57
+ }
58
+
59
+ /**
60
+ * Cleanup
61
+ */
62
+ destroy() {
63
+ if (this._unsubscribe) {
64
+ this._unsubscribe()
65
+ this._unsubscribe = null
66
+ }
67
+ }
68
+
69
+ // ═══════════════════════════════════════════════════════════════════════════
70
+ // Stack change handling
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+
73
+ /**
74
+ * Handle stack change - rebuild hydrated levels
75
+ * @param {import('./ActiveStack.js').StackLevel[]} stackLevels
76
+ * @private
77
+ */
78
+ _onStackChange(stackLevels) {
79
+ // Create new hydrated levels (each starts its own async hydration)
80
+ this._levels = stackLevels.map((level, index) =>
81
+ this._createHydratedLevel(level, index)
82
+ )
83
+ // Don't emit here - each level will emit when hydration completes
84
+ }
85
+
86
+ /**
87
+ * Create a hydrated level with its own promise
88
+ * @param {import('./ActiveStack.js').StackLevel} stackLevel
89
+ * @param {number} index
90
+ * @returns {HydratedLevel}
91
+ * @private
92
+ */
93
+ _createHydratedLevel(stackLevel, index) {
94
+ const level = {
95
+ // Sync config (copied from stack)
96
+ entity: stackLevel.entity,
97
+ param: stackLevel.param,
98
+ foreignKey: stackLevel.foreignKey,
99
+ id: stackLevel.id,
100
+
101
+ // Async state
102
+ promise: null,
103
+ loading: false,
104
+ hydrated: false,
105
+ error: null,
106
+ data: null,
107
+ label: null,
108
+ }
109
+
110
+ // Start hydration and store promise
111
+ level.promise = this._hydrate(level)
112
+
113
+ return level
114
+ }
115
+
116
+ /**
117
+ * Hydrate a single level
118
+ * @param {HydratedLevel} level
119
+ * @returns {Promise<void>}
120
+ * @private
121
+ */
122
+ async _hydrate(level) {
123
+ // All levels in stack have IDs (that's the new contract)
124
+ // But check anyway for safety
125
+ if (!level.id) {
126
+ const manager = this._orchestrator?.get(level.entity)
127
+ level.label = manager?.labelPlural ?? level.entity
128
+ level.hydrated = true
129
+ this._emitChange()
130
+ return
131
+ }
132
+
133
+ level.loading = true
134
+ // Don't emit on loading start - reduces signal noise
135
+
136
+ try {
137
+ const manager = this._orchestrator?.get(level.entity)
138
+ if (!manager) {
139
+ level.label = level.id
140
+ level.hydrated = true
141
+ level.loading = false
142
+ this._emitChange()
143
+ return
144
+ }
145
+
146
+ // Fetch entity data
147
+ level.data = await manager.get(level.id)
148
+ level.label = manager.getEntityLabel?.(level.data) ?? level.id
149
+ level.hydrated = true
150
+ } catch (err) {
151
+ level.error = err
152
+ level.label = level.id // Fallback to ID
153
+ level.hydrated = true // Mark as hydrated even on error
154
+ } finally {
155
+ level.loading = false
156
+ this._emitChange()
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Emit hydration change signal
162
+ * @private
163
+ */
164
+ _emitChange() {
165
+ this._emit('stack:hydration:change', { levels: this._levels })
166
+ }
167
+
168
+ // ═══════════════════════════════════════════════════════════════════════════
169
+ // Accessors
170
+ // ═══════════════════════════════════════════════════════════════════════════
171
+
172
+ /**
173
+ * All hydrated levels
174
+ * @returns {HydratedLevel[]}
175
+ */
176
+ getLevels() {
177
+ return this._levels
178
+ }
179
+
180
+ /**
181
+ * Get hydrated level by index
182
+ * @param {number} index
183
+ * @returns {HydratedLevel|null}
184
+ */
185
+ getLevel(index) {
186
+ return this._levels[index] ?? null
187
+ }
188
+
189
+ /**
190
+ * Get hydrated level by entity name
191
+ * @param {string} entity
192
+ * @returns {HydratedLevel|null}
193
+ */
194
+ getLevelByEntity(entity) {
195
+ return this._levels.find(l => l.entity === entity) ?? null
196
+ }
197
+
198
+ /**
199
+ * Current hydrated level
200
+ * @returns {HydratedLevel|null}
201
+ */
202
+ getCurrent() {
203
+ return this._levels.at(-1) ?? null
204
+ }
205
+
206
+ /**
207
+ * Parent hydrated level
208
+ * @returns {HydratedLevel|null}
209
+ */
210
+ getParent() {
211
+ return this._levels.at(-2) ?? null
212
+ }
213
+
214
+ /**
215
+ * Root hydrated level
216
+ * @returns {HydratedLevel|null}
217
+ */
218
+ getRoot() {
219
+ return this._levels[0] ?? null
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════════════════════════
223
+ // Manual update (for pages that load their own data)
224
+ // ═══════════════════════════════════════════════════════════════════════════
225
+
226
+ /**
227
+ * Manually set data for current level (skips fetch)
228
+ * Used by pages that already loaded the entity
229
+ * @param {Record<string, any>} data
230
+ */
231
+ setCurrentData(data) {
232
+ const level = this.getCurrent()
233
+ if (!level) return
234
+
235
+ const manager = this._orchestrator?.get(level.entity)
236
+ level.data = data
237
+ level.label = manager?.getEntityLabel?.(data) ?? level.id
238
+ level.hydrated = true
239
+ level.loading = false
240
+ this._emit('stack:hydrated', { level })
241
+ this._emit('stack:hydration:change', { levels: this._levels })
242
+ }
243
+
244
+ /**
245
+ * Manually set data for a level by entity name
246
+ * @param {string} entity
247
+ * @param {Record<string, any>} data
248
+ */
249
+ setEntityData(entity, data) {
250
+ const level = this.getLevelByEntity(entity)
251
+ if (!level) return
252
+
253
+ const manager = this._orchestrator?.get(entity)
254
+ level.data = data
255
+ level.label = manager?.getEntityLabel?.(data) ?? level.id
256
+ level.hydrated = true
257
+ level.loading = false
258
+ this._emit('stack:hydrated', { level })
259
+ this._emit('stack:hydration:change', { levels: this._levels })
260
+ }
261
+
262
+ // ═══════════════════════════════════════════════════════════════════════════
263
+ // Internal
264
+ // ═══════════════════════════════════════════════════════════════════════════
265
+
266
+ /**
267
+ * Emit event via SignalBus
268
+ * @param {string} signal
269
+ * @param {*} payload
270
+ * @private
271
+ */
272
+ _emit(signal, payload) {
273
+ if (this._signalBus) {
274
+ this._signalBus.emit(signal, payload)
275
+ }
276
+ }
277
+ }
278
+
279
+ export default StackHydrator
@@ -2,11 +2,18 @@
2
2
  * Chain Module - Active navigation stack management
3
3
  *
4
4
  * Provides:
5
- * - ActiveStack: Reactive container for current navigation stack
6
- * - useActiveStack: Composable to build and access the stack
5
+ * - ActiveStack: Sync container for navigation context (entity, param, foreignKey, id)
6
+ * - StackHydrator: Async layer for entity data and labels
7
+ * - useActiveStack: Composable to build and access the sync stack
8
+ * - useStackHydrator: Composable to access hydrated data
7
9
  *
8
10
  * @module chain
9
11
  */
10
12
 
13
+ // Sync stack (context only)
11
14
  export { ActiveStack } from './ActiveStack.js'
12
15
  export { useActiveStack } from './useActiveStack.js'
16
+
17
+ // Async hydration (data + labels)
18
+ export { StackHydrator } from './StackHydrator.js'
19
+ export { useStackHydrator } from './useStackHydrator.js'