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,123 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * ObjectTree - VSCode-style collapsible object explorer
4
- *
5
- * Renders objects/arrays as a tree with collapsible nodes.
6
- * Primitive values are shown inline, objects/arrays are expandable.
7
- */
8
- import { ref, computed } from 'vue'
9
-
10
- type DataValue = Record<string, unknown> | unknown[] | string | number | boolean | null | undefined
11
-
12
- const props = withDefaults(defineProps<{
13
- data?: DataValue
14
- depth?: number
15
- maxDepth?: number
16
- name?: string | null
17
- defaultExpanded?: boolean
18
- }>(), {
19
- data: null,
20
- depth: 0,
21
- maxDepth: 10,
22
- name: null,
23
- defaultExpanded: false
24
- })
25
-
26
- const expanded = ref<boolean>(props.defaultExpanded && props.depth < 2)
27
-
28
- const isObject = computed<boolean>(() => props.data !== null && typeof props.data === 'object')
29
- const isArray = computed<boolean>(() => Array.isArray(props.data))
30
- const isEmpty = computed<boolean>(() => {
31
- if (!isObject.value) return false
32
- return isArray.value ? (props.data as unknown[]).length === 0 : Object.keys(props.data as object).length === 0
33
- })
34
-
35
- const keys = computed<string[]>(() => {
36
- if (!isObject.value) return []
37
- return Object.keys(props.data as object).slice(0, 100)
38
- })
39
-
40
- const preview = computed<string>(() => {
41
- if (!isObject.value) return ''
42
- if (isArray.value) {
43
- return `Array(${(props.data as unknown[]).length})`
44
- }
45
- const dataObj = props.data as Record<string, unknown>
46
- const keyCount = Object.keys(dataObj).length
47
- const firstKeys = Object.keys(dataObj).slice(0, 3).join(', ')
48
- return keyCount <= 3 ? `{${firstKeys}}` : `{${firstKeys}, ...}`
49
- })
50
-
51
- const valueClass = computed<string>(() => {
52
- if (props.data === null) return 'obj-null'
53
- if (props.data === undefined) return 'obj-undefined'
54
- switch (typeof props.data) {
55
- case 'string': return 'obj-string'
56
- case 'number': return 'obj-number'
57
- case 'boolean': return 'obj-boolean'
58
- case 'function': return 'obj-function'
59
- default: return ''
60
- }
61
- })
62
-
63
- function formatValue(val: DataValue): string {
64
- if (val === null) return 'null'
65
- if (val === undefined) return 'undefined'
66
- if (typeof val === 'string') {
67
- const truncated = val.length > 100 ? val.slice(0, 100) + '...' : val
68
- return `"${truncated}"`
69
- }
70
- if (typeof val === 'function') return 'ƒ()'
71
- return String(val)
72
- }
73
-
74
- function toggleExpand(): void {
75
- if (isObject.value && !isEmpty.value) {
76
- expanded.value = !expanded.value
77
- }
78
- }
79
- </script>
80
-
81
- <template>
82
- <div class="obj-node" :style="{ paddingLeft: depth > 0 ? '12px' : '0' }">
83
- <!-- Expandable object/array -->
84
- <template v-if="isObject && !isEmpty">
85
- <div class="obj-line" @click="toggleExpand">
86
- <span class="obj-toggle" :class="{ 'obj-expanded': expanded }">▶</span>
87
- <span v-if="name !== null" class="obj-key">{{ name }}:</span>
88
- <span class="obj-preview">{{ preview }}</span>
89
- </div>
90
- <div v-if="expanded && depth < maxDepth" class="obj-children">
91
- <ObjectTree
92
- v-for="key in keys"
93
- :key="key"
94
- :data="data && typeof data === 'object' ? (data as any)[key] : undefined"
95
- :name="key"
96
- :depth="depth + 1"
97
- :maxDepth="maxDepth"
98
- />
99
- <div v-if="data && typeof data === 'object' && Object.keys(data).length > 100" class="obj-truncated">
100
- ... {{ Object.keys(data as object).length - 100 }} more
101
- </div>
102
- </div>
103
- <div v-else-if="expanded" class="obj-max-depth">[Max depth]</div>
104
- </template>
105
-
106
- <!-- Empty object/array -->
107
- <template v-else-if="isObject && isEmpty">
108
- <div class="obj-line">
109
- <span v-if="name !== null" class="obj-key">{{ name }}:</span>
110
- <span class="obj-empty">{{ isArray ? '[]' : '{}' }}</span>
111
- </div>
112
- </template>
113
-
114
- <!-- Primitive value -->
115
- <template v-else>
116
- <div class="obj-line">
117
- <span v-if="name !== null" class="obj-key">{{ name }}:</span>
118
- <span :class="valueClass">{{ formatValue(data) }}</span>
119
- </div>
120
- </template>
121
- </div>
122
- </template>
123
-
@@ -1,100 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * EntriesPanel - Default entries display (horizontal/vertical)
4
- */
5
- import { ref, type Ref } from 'vue'
6
- import ObjectTree from '../ObjectTree.vue'
7
-
8
- interface DebugEntry {
9
- timestamp: number
10
- _isNew?: boolean
11
- name?: string
12
- message?: string
13
- [key: string]: unknown
14
- }
15
-
16
- type EntriesPanelMode = 'horizontal' | 'vertical'
17
-
18
- interface Props {
19
- entries: DebugEntry[]
20
- mode?: EntriesPanelMode
21
- maxEntries?: number
22
- }
23
-
24
- withDefaults(defineProps<Props>(), {
25
- mode: 'vertical',
26
- maxEntries: 10
27
- })
28
-
29
- const copiedIdx: Ref<number | null> = ref(null)
30
-
31
- function formatTime(ts: number): string {
32
- return new Date(ts).toLocaleTimeString('en-US', {
33
- hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
34
- })
35
- }
36
-
37
- function getEntryData(entry: DebugEntry): Record<string, unknown> {
38
- const { timestamp: _, _isNew: __, ...rest } = entry
39
- void _
40
- void __
41
- return rest
42
- }
43
-
44
- async function copyEntry(entry: DebugEntry, idx: number): Promise<void> {
45
- try {
46
- const data = getEntryData(entry)
47
- await navigator.clipboard.writeText(JSON.stringify(data, null, 2))
48
- copiedIdx.value = idx
49
- setTimeout(() => { copiedIdx.value = null }, 1500)
50
- } catch (e) {
51
- console.error('Failed to copy:', e)
52
- }
53
- }
54
- </script>
55
-
56
- <template>
57
- <!-- Horizontal layout -->
58
- <div v-if="mode === 'horizontal'" class="entries-h">
59
- <div
60
- v-for="(entry, idx) in entries.slice().reverse().slice(0, maxEntries)"
61
- :key="idx"
62
- class="entry-h"
63
- :class="{ 'entry-new': entry._isNew }"
64
- >
65
- <div class="entry-meta">
66
- <span v-if="entry._isNew" class="entry-new-dot" title="New (unseen)" />
67
- <span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
68
- <span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
69
- </div>
70
- <ObjectTree :data="getEntryData(entry)" :maxDepth="3" />
71
- </div>
72
- </div>
73
-
74
- <!-- Vertical layout -->
75
- <div v-else class="entries-v">
76
- <div
77
- v-for="(entry, idx) in entries.slice().reverse()"
78
- :key="idx"
79
- class="entry-v"
80
- :class="{ 'entry-new': entry._isNew }"
81
- >
82
- <div class="entry-header">
83
- <span v-if="entry._isNew" class="entry-new-dot" title="New (unseen)" />
84
- <span class="entry-time">{{ formatTime(entry.timestamp) }}</span>
85
- <span v-if="entry.name" class="entry-name">{{ entry.name }}</span>
86
- <span v-if="entry.message" class="entry-message">{{ entry.message }}</span>
87
- <button
88
- class="entry-copy"
89
- :class="{ 'entry-copied': copiedIdx === idx }"
90
- @click="copyEntry(entry, idx)"
91
- :title="copiedIdx === idx ? 'Copied!' : 'Copy to clipboard'"
92
- >
93
- <i :class="['pi', copiedIdx === idx ? 'pi-check' : 'pi-copy']" />
94
- </button>
95
- </div>
96
- <ObjectTree :data="getEntryData(entry)" :maxDepth="6" />
97
- </div>
98
- </div>
99
- </template>
100
-
@@ -1,188 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * SignalsPanel - Debug panel for signals with pattern filter
4
- *
5
- * Supports QuarKernel wildcard patterns:
6
- * - auth:** → all auth signals (auth:login, auth:impersonate:start)
7
- * - cache:** → all cache signals
8
- * - *:created → all creation signals
9
- * - ** → all signals (default)
10
- */
11
- import { ref, computed, watch } from 'vue'
12
- import ObjectTree from '../ObjectTree.vue'
13
-
14
- interface SignalEntry {
15
- name: string
16
- timestamp: number
17
- data?: Record<string, unknown>
18
- _isNew?: boolean
19
- }
20
-
21
- interface SignalCollector {
22
- // Collector interface - minimal typing for now
23
- [key: string]: unknown
24
- }
25
-
26
- interface FilterPreset {
27
- label: string
28
- pattern: string
29
- }
30
-
31
- const props = defineProps<{
32
- collector: SignalCollector
33
- entries: SignalEntry[]
34
- }>()
35
-
36
- // Filter state - persisted in localStorage
37
- const STORAGE_KEY = 'qdadm-signals-filter'
38
- const STORAGE_KEY_MAX = 'qdadm-signals-max'
39
- const filterPattern = ref<string>(localStorage.getItem(STORAGE_KEY) || '')
40
- const maxSignals = ref<number>(parseInt(localStorage.getItem(STORAGE_KEY_MAX) || '50') || 50)
41
-
42
- watch(filterPattern, (val) => {
43
- localStorage.setItem(STORAGE_KEY, val)
44
- })
45
-
46
- watch(maxSignals, (val) => {
47
- localStorage.setItem(STORAGE_KEY_MAX, String(val))
48
- })
49
-
50
- // Convert wildcard pattern to regex
51
- function wildcardToRegex(pattern: string): RegExp | null {
52
- if (!pattern || pattern === '**') return null // No filter
53
-
54
- // Escape regex special chars except * and :
55
- const regex = pattern
56
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
57
- // ** matches anything (including colons)
58
- .replace(/\*\*/g, '.*')
59
- // * matches anything except colon
60
- .replace(/\*/g, '[^:]*')
61
-
62
- return new RegExp(`^${regex}$`)
63
- }
64
-
65
- const filterRegex = computed<RegExp | null>(() => wildcardToRegex(filterPattern.value.trim()))
66
-
67
- // Apply filter, max limit, and reverse for top-down (newest first)
68
- const filteredEntries = computed<SignalEntry[]>(() => {
69
- let result: SignalEntry[] = props.entries
70
- if (filterRegex.value) {
71
- result = result.filter((e: SignalEntry) => filterRegex.value!.test(e.name))
72
- }
73
- // Apply max limit (slice from end to keep newest)
74
- if (maxSignals.value > 0 && result.length > maxSignals.value) {
75
- result = result.slice(-maxSignals.value)
76
- }
77
- // Reverse for top-down display (newest first)
78
- return [...result].reverse()
79
- })
80
-
81
- const filterStats = computed<string>(() => {
82
- const total = props.entries.length
83
- const shown = filteredEntries.value.length
84
- return total !== shown ? `${shown}/${total}` : `${total}`
85
- })
86
-
87
- // Preset filters
88
- const presets: FilterPreset[] = [
89
- { label: 'All', pattern: '' },
90
- { label: 'data', pattern: 'entity:data-invalidate' },
91
- { label: 'datalayer', pattern: 'entity:datalayer-invalidate' },
92
- { label: 'auth', pattern: 'auth:**' },
93
- { label: 'entity', pattern: 'entity:**' },
94
- { label: 'toast', pattern: 'toast:**' }
95
- ]
96
-
97
- function applyPreset(pattern: string): void {
98
- filterPattern.value = pattern
99
- }
100
-
101
- function formatTime(ts: number): string {
102
- const d = new Date(ts)
103
- return d.toLocaleTimeString('en-US', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0')
104
- }
105
-
106
- // Extract domain from signal name (first segment)
107
- function getDomain(name: string): string {
108
- return name.split(':')[0] ?? ''
109
- }
110
-
111
- const domainColors: Record<string, string> = {
112
- auth: '#10b981',
113
- cache: '#f59e0b',
114
- entity: '#3b82f6',
115
- toast: '#8b5cf6',
116
- route: '#06b6d4',
117
- error: '#ef4444'
118
- }
119
-
120
- function getDomainColor(name: string): string {
121
- const domain = getDomain(name)
122
- return domainColors[domain] || '#6b7280'
123
- }
124
- </script>
125
-
126
- <template>
127
- <div class="signals-panel">
128
- <!-- Filter bar -->
129
- <div class="signals-filter">
130
- <div class="signals-filter-input">
131
- <i class="pi pi-filter" />
132
- <input
133
- v-model="filterPattern"
134
- type="text"
135
- placeholder="auth:** cache:** *:created"
136
- class="filter-input"
137
- />
138
- <span class="signals-count">{{ filterStats }}</span>
139
- <span class="signals-max-label">max</span>
140
- <input
141
- v-model.number="maxSignals"
142
- type="number"
143
- min="10"
144
- max="500"
145
- class="max-input"
146
- />
147
- </div>
148
- <div class="signals-presets">
149
- <button
150
- v-for="p in presets"
151
- :key="p.pattern"
152
- class="debug-toolbar-btn"
153
- :class="{ 'debug-toolbar-btn--active': filterPattern === p.pattern }"
154
- @click="applyPreset(p.pattern)"
155
- >
156
- {{ p.label }}
157
- </button>
158
- </div>
159
- </div>
160
-
161
- <!-- Entries list -->
162
- <div class="signals-list">
163
- <div v-if="filteredEntries.length === 0" class="signals-empty">
164
- <i class="pi pi-inbox" />
165
- <span>{{ entries.length === 0 ? 'No signals' : 'No matching signals' }}</span>
166
- </div>
167
-
168
- <div
169
- v-for="(entry, idx) in filteredEntries"
170
- :key="idx"
171
- class="signal-entry"
172
- :class="{ 'signal-new': entry._isNew }"
173
- >
174
- <div class="signal-header">
175
- <span v-if="entry._isNew" class="signal-new-dot" title="New (unseen)" />
176
- <span class="signal-time">{{ formatTime(entry.timestamp) }}</span>
177
- <span class="signal-name" :style="{ color: getDomainColor(entry.name) }">
178
- {{ entry.name }}
179
- </span>
180
- </div>
181
- <div v-if="entry.data && Object.keys(entry.data).length > 0" class="signal-data">
182
- <ObjectTree :data="entry.data" :expanded="false" />
183
- </div>
184
- </div>
185
- </div>
186
- </div>
187
- </template>
188
-
@@ -1,45 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * ToastsPanel - Toasts collector display (vertical mode)
4
- */
5
- interface ToastEntry {
6
- timestamp: number
7
- severity: string
8
- summary?: string
9
- detail?: string
10
- emitter?: string
11
- _isNew?: boolean
12
- }
13
-
14
- interface Props {
15
- entries: ToastEntry[]
16
- }
17
-
18
- defineProps<Props>()
19
-
20
- function formatTime(ts: number): string {
21
- return new Date(ts).toLocaleTimeString('en-US', {
22
- hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
23
- })
24
- }
25
- </script>
26
-
27
- <template>
28
- <div class="toasts-panel">
29
- <div
30
- v-for="(entry, idx) in entries.slice().reverse()"
31
- :key="idx"
32
- class="toast-entry"
33
- :class="[`toast-${entry.severity}`, { 'entry-new': entry._isNew }]"
34
- >
35
- <div class="toast-header">
36
- <span class="toast-time">{{ formatTime(entry.timestamp) }}</span>
37
- <span class="toast-severity" :class="`toast-severity-${entry.severity}`">{{ entry.severity }}</span>
38
- <span v-if="entry.summary" class="toast-summary">{{ entry.summary }}</span>
39
- <span v-if="entry.emitter" class="toast-emitter">{{ entry.emitter }}</span>
40
- </div>
41
- <div v-if="entry.detail" class="toast-detail">{{ entry.detail }}</div>
42
- </div>
43
- </div>
44
- </template>
45
-