qdadm 1.13.1 → 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.
- package/package.json +7 -3
- package/src/chain/ActiveStack.ts +79 -98
- package/src/chain/StackHydrator.ts +3 -2
- package/src/chain/index.ts +7 -1
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/edit/FormActions.vue +9 -6
- package/src/components/edit/LookupPickerDialog.vue +6 -3
- package/src/components/index.ts +6 -0
- package/src/composables/useEntityItemFormPage.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +1 -0
- package/src/composables/useFieldManager.ts +100 -3
- package/src/composables/useListPage.ts +50 -59
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavigation.ts +26 -3
- package/src/composables/useOptionsLookup.ts +5 -1
- package/src/gen/generateManagers.test.js +27 -0
- package/src/gen/generateManagers.ts +12 -0
- package/src/hooks/HookRegistry.ts +14 -435
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +37 -0
- package/src/kernel/EventRouter.ts +17 -300
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +6 -0
- package/src/kernel/Kernel.registries.ts +10 -2
- package/src/kernel/Kernel.routing.ts +43 -1
- package/src/kernel/Kernel.ts +43 -0
- package/src/kernel/Kernel.types.ts +52 -1
- package/src/kernel/Kernel.vue.ts +121 -15
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +125 -834
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/SSEBridge.ts +7 -362
- package/src/kernel/SignalBus.ts +24 -148
- package/src/modules/debug/AuthCollector.ts +48 -1
- package/src/modules/debug/Collector.ts +16 -302
- package/src/modules/debug/DebugBridge.ts +10 -171
- package/src/modules/debug/DebugModule.ts +35 -5
- package/src/modules/debug/EntitiesCollector.ts +97 -1
- package/src/modules/debug/ErrorCollector.ts +2 -77
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +3 -147
- package/src/modules/debug/RouterCollector.ts +101 -1
- package/src/modules/debug/SignalCollector.ts +2 -150
- package/src/modules/debug/ToastCollector.ts +2 -91
- package/src/modules/debug/ZonesCollector.ts +93 -1
- package/src/modules/debug/components/DebugBar.vue +19 -775
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +4 -3
- package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
- package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
- package/src/modules/debug/components/panels/index.ts +10 -4
- package/src/modules/debug/index.ts +15 -0
- package/src/modules/debug/styles.scss +22 -18
- package/src/utils/index.ts +0 -3
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +16 -0
- package/src/modules/debug/components/ObjectTree.vue +0 -123
- package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
- package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
- package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
- package/src/utils/debugInjector.ts +0 -306
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdadm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./styles": "./src/styles/index.scss",
|
|
37
37
|
"./styles/variables": "./src/styles/_variables.scss",
|
|
38
38
|
"./gen": "./src/gen/index.ts",
|
|
39
|
-
"./gen/vite-plugin": "./src/gen/vite-plugin.ts"
|
|
39
|
+
"./gen/vite-plugin": "./src/gen/vite-plugin.ts",
|
|
40
|
+
"./vite-plugin-debug": "./src/vite/qdadmDebugPlugin.ts"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"src",
|
|
@@ -44,8 +45,11 @@
|
|
|
44
45
|
"LICENSE"
|
|
45
46
|
],
|
|
46
47
|
"dependencies": {
|
|
48
|
+
"@quazardous/qdcore": "^0.2.1",
|
|
49
|
+
"@quazardous/qddebug": "^0.2.1",
|
|
47
50
|
"@quazardous/quarkernel": "^2.1.0",
|
|
48
|
-
"pluralize": "^8.0.0"
|
|
51
|
+
"pluralize": "^8.0.0",
|
|
52
|
+
"yaml": "^2.8.4"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
|
51
55
|
"pinia": "^2.0.0 || ^3.0.0",
|
package/src/chain/ActiveStack.ts
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ActiveStack - Sync navigation context from route
|
|
2
|
+
* ActiveStack - Sync navigation context from route (qdadm flavour).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Composes a generic `Stack<EntityStackLevel>` from `@quazardous/qdcore` and
|
|
5
|
+
* adds qdadm-specific lookups (e.g. `getLevelByEntity`). The underlying levels
|
|
6
|
+
* dual-carry `name` (qdcore-aligned) and `entity` (legacy qdadm field), so all
|
|
7
|
+
* existing consumers continue to read `level.entity` unchanged.
|
|
6
8
|
*
|
|
7
9
|
* Stack only contains levels WITH IDs (entities with context).
|
|
8
10
|
* - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
|
|
9
11
|
* - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
|
|
10
12
|
*
|
|
11
|
-
* Signals emitted:
|
|
12
|
-
* - stack:change
|
|
13
|
+
* Signals emitted (via SignalBus):
|
|
14
|
+
* - stack:change — when stack levels change
|
|
13
15
|
*
|
|
14
|
-
* For Vue reactivity, use useActiveStack composable.
|
|
16
|
+
* For Vue reactivity, use the `useActiveStack` composable.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
19
|
* const stack = new ActiveStack(signalBus)
|
|
18
20
|
* signalBus.on('stack:change', ({ levels }) => console.log('Stack:', levels))
|
|
19
|
-
* stack.set([{ entity: 'bots', param: 'uuid', id: 'bot-123' }])
|
|
21
|
+
* stack.set([{ entity: 'bots', param: 'uuid', foreignKey: null, id: 'bot-123' }])
|
|
20
22
|
*/
|
|
21
23
|
|
|
24
|
+
import { Stack, type ContentStackLevel } from '@quazardous/qdcore'
|
|
22
25
|
import type { SignalBus } from '../kernel/SignalBus'
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
*
|
|
28
|
+
* qdadm stack level: entity-bound `ContentStackLevel`.
|
|
29
|
+
*
|
|
30
|
+
* `name` and `entity` always carry the same value. `name` is the qdcore-canonical
|
|
31
|
+
* field; `entity` is preserved for backwards compatibility with qdadm callers.
|
|
26
32
|
*/
|
|
27
|
-
export interface
|
|
28
|
-
|
|
33
|
+
export interface EntityStackLevel extends ContentStackLevel {
|
|
34
|
+
type: 'entity'
|
|
35
|
+
/** qdcore-aligned name (= entity) */
|
|
36
|
+
name: string
|
|
37
|
+
/** Entity name (e.g., 'bots', 'commands') — alias of `name` */
|
|
29
38
|
entity: string
|
|
30
39
|
/** Route param name (e.g., 'uuid', 'id') */
|
|
31
40
|
param: string
|
|
@@ -35,138 +44,110 @@ export interface StackLevel {
|
|
|
35
44
|
id: string | null
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Legacy alias kept for backwards compatibility. New code should prefer
|
|
49
|
+
* {@link EntityStackLevel}.
|
|
50
|
+
*/
|
|
51
|
+
export type StackLevel = EntityStackLevel
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Input shape accepted by {@link ActiveStack.set} — `name` is optional and
|
|
55
|
+
* defaults to `entity` so existing callers (`{ entity, param, foreignKey, id }`)
|
|
56
|
+
* keep working without changes.
|
|
57
|
+
*/
|
|
58
|
+
export type EntityStackLevelInput = Omit<EntityStackLevel, 'type' | 'name'> & {
|
|
59
|
+
type?: 'entity'
|
|
60
|
+
name?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* Stack change event payload
|
|
40
65
|
*/
|
|
41
66
|
export interface StackChangePayload {
|
|
42
|
-
levels:
|
|
67
|
+
levels: EntityStackLevel[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Normalize loose input into a fully-typed EntityStackLevel.
|
|
72
|
+
*/
|
|
73
|
+
function normalize(input: EntityStackLevelInput): EntityStackLevel {
|
|
74
|
+
return {
|
|
75
|
+
type: 'entity',
|
|
76
|
+
name: input.name ?? input.entity,
|
|
77
|
+
entity: input.entity,
|
|
78
|
+
param: input.param,
|
|
79
|
+
foreignKey: input.foreignKey,
|
|
80
|
+
id: input.id,
|
|
81
|
+
}
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
export class ActiveStack {
|
|
46
|
-
private
|
|
47
|
-
private _signalBus: SignalBus | null
|
|
85
|
+
private _stack: Stack<EntityStackLevel>
|
|
48
86
|
|
|
49
87
|
/**
|
|
50
88
|
* @param signalBus - Optional signal bus for events
|
|
51
89
|
*/
|
|
52
90
|
constructor(signalBus: SignalBus | null = null) {
|
|
53
|
-
this.
|
|
91
|
+
this._stack = new Stack<EntityStackLevel>(signalBus)
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
//
|
|
57
|
-
// Mutators
|
|
58
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Replace entire stack (called on route change)
|
|
62
|
-
* Only emits if levels actually changed (prevents duplicate emissions)
|
|
63
|
-
*
|
|
64
|
-
* BUG: Still getting duplicate signals (4 instead of 2) on navigation.
|
|
65
|
-
* Equality check should prevent this but something is triggering multiple calls
|
|
66
|
-
* with different levels. Need to investigate router.afterEach timing.
|
|
67
|
-
*/
|
|
68
|
-
set(levels: StackLevel[]): void {
|
|
69
|
-
// Quick equality check - same length and same entity+id pairs
|
|
70
|
-
if (this._levelsEqual(levels)) {
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
this._levels = levels
|
|
74
|
-
this._emit('stack:change', { levels: this._levels })
|
|
75
|
-
}
|
|
94
|
+
// ─── Mutators ─────────────────────────────────────────────────────────────
|
|
76
95
|
|
|
77
96
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
97
|
+
* Replace entire stack (called on route change).
|
|
98
|
+
* Only emits if levels actually changed (handled by qdcore Stack).
|
|
99
|
+
* Accepts either fully-typed `EntityStackLevel` or the legacy
|
|
100
|
+
* `{ entity, param, foreignKey, id }` shape.
|
|
80
101
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (let i = 0; i < this._levels.length; i++) {
|
|
84
|
-
const curr = this._levels[i]
|
|
85
|
-
const next = newLevels[i]
|
|
86
|
-
// Both are guaranteed to exist because we checked lengths match
|
|
87
|
-
if (!curr || !next) continue
|
|
88
|
-
if (curr.entity !== next.entity || curr.id !== next.id) {
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return true
|
|
102
|
+
set(levels: EntityStackLevelInput[]): void {
|
|
103
|
+
this._stack.set(levels.map(normalize))
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/**
|
|
96
|
-
* Clear the stack
|
|
97
|
-
* Only emits if not already empty
|
|
107
|
+
* Clear the stack.
|
|
98
108
|
*/
|
|
99
109
|
clear(): void {
|
|
100
|
-
|
|
101
|
-
this._levels = []
|
|
102
|
-
this._emit('stack:change', { levels: this._levels })
|
|
110
|
+
this._stack.clear()
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
//
|
|
106
|
-
// Accessors
|
|
107
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// ─── Accessors ────────────────────────────────────────────────────────────
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*/
|
|
112
|
-
getLevels(): StackLevel[] {
|
|
113
|
-
return this._levels
|
|
115
|
+
getLevels(): EntityStackLevel[] {
|
|
116
|
+
return this._stack.getLevels()
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*/
|
|
119
|
-
getLevel(index: number): StackLevel | null {
|
|
120
|
-
return this._levels[index] ?? null
|
|
119
|
+
getLevel(index: number): EntityStackLevel | null {
|
|
120
|
+
return this._stack.getLevel(index)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* Get level by entity name
|
|
124
|
+
* Get level by entity name (qdadm-specific helper).
|
|
125
125
|
*/
|
|
126
|
-
getLevelByEntity(entity: string):
|
|
127
|
-
return this.
|
|
126
|
+
getLevelByEntity(entity: string): EntityStackLevel | null {
|
|
127
|
+
return this._stack.getLevels().find(l => l.entity === entity) ?? null
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*/
|
|
133
|
-
getCurrent(): StackLevel | null {
|
|
134
|
-
return this._levels.at(-1) ?? null
|
|
130
|
+
getCurrent(): EntityStackLevel | null {
|
|
131
|
+
return this._stack.getCurrent()
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
*/
|
|
140
|
-
getParent(): StackLevel | null {
|
|
141
|
-
return this._levels.at(-2) ?? null
|
|
134
|
+
getParent(): EntityStackLevel | null {
|
|
135
|
+
return this._stack.getParent()
|
|
142
136
|
}
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*/
|
|
147
|
-
getRoot(): StackLevel | null {
|
|
148
|
-
return this._levels[0] ?? null
|
|
138
|
+
getRoot(): EntityStackLevel | null {
|
|
139
|
+
return this._stack.getRoot()
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
/**
|
|
152
|
-
* Stack depth
|
|
153
|
-
*/
|
|
154
142
|
getDepth(): number {
|
|
155
|
-
return this.
|
|
143
|
+
return this._stack.getDepth()
|
|
156
144
|
}
|
|
157
145
|
|
|
158
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
|
-
// Internal
|
|
160
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
-
|
|
162
146
|
/**
|
|
163
|
-
*
|
|
164
|
-
* @private
|
|
147
|
+
* Underlying qdcore Stack instance (escape hatch for advanced use).
|
|
165
148
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this._signalBus.emit(signal, payload)
|
|
169
|
-
}
|
|
149
|
+
getCoreStack(): Stack<EntityStackLevel> {
|
|
150
|
+
return this._stack
|
|
170
151
|
}
|
|
171
152
|
}
|
|
172
153
|
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* signalBus.on('stack:hydrated', (event) => console.log('Hydrated:', event.data.levels))
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
17
|
+
import type { Hydrator } from '@quazardous/qdcore'
|
|
18
|
+
import type { ActiveStack, EntityStackLevel, StackLevel } from './ActiveStack'
|
|
18
19
|
import type { Orchestrator } from '../orchestrator/Orchestrator'
|
|
19
20
|
import type { SignalBus } from '../kernel/SignalBus'
|
|
20
21
|
import type { EntityRecord } from '../types'
|
|
@@ -52,7 +53,7 @@ export interface HydratedPayload {
|
|
|
52
53
|
levels: HydratedLevel[]
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
export class StackHydrator {
|
|
56
|
+
export class StackHydrator implements Hydrator<EntityStackLevel, HydratedLevel> {
|
|
56
57
|
private _activeStack: ActiveStack
|
|
57
58
|
private _orchestrator: Orchestrator
|
|
58
59
|
private _signalBus: SignalBus | null
|
package/src/chain/index.ts
CHANGED
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Sync stack (context only)
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
ActiveStack,
|
|
16
|
+
type StackLevel,
|
|
17
|
+
type EntityStackLevel,
|
|
18
|
+
type EntityStackLevelInput,
|
|
19
|
+
type StackChangePayload,
|
|
20
|
+
} from './ActiveStack'
|
|
15
21
|
export { useActiveStack, type UseActiveStackReturn } from './useActiveStack'
|
|
16
22
|
|
|
17
23
|
// Async hydration (data + labels)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* QdadmRoot — drop-in helper for hosts that own their own Vue app
|
|
4
|
+
* and want qdadm's DOM extras (Toast, ToastListener, DebugBar)
|
|
5
|
+
* rendered alongside.
|
|
6
|
+
*
|
|
7
|
+
* Equivalent to the wrapper Kernel produces internally when it owns
|
|
8
|
+
* `createApp`. When `Kernel({ existingApp })` is used, the host's
|
|
9
|
+
* App.vue should render `<QdadmRoot />` somewhere visible (typically
|
|
10
|
+
* at the end of the template) so toasts and the debug bar appear.
|
|
11
|
+
*
|
|
12
|
+
* The Toast / DebugBar children are conditional on what the Kernel
|
|
13
|
+
* was configured with (PrimeVue presence, debugBar option) — render
|
|
14
|
+
* is a no-op otherwise, so it's always safe to include.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { computed, inject, type Component } from 'vue'
|
|
18
|
+
import Toast from 'primevue/toast'
|
|
19
|
+
import ToastListener from '../toast/ToastListener.vue'
|
|
20
|
+
import { getQdadmDebugBarRef } from '../kernel/Kernel.vue'
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(
|
|
23
|
+
defineProps<{
|
|
24
|
+
/**
|
|
25
|
+
* Render the qdadm debug bar inline. Default true preserves the
|
|
26
|
+
* legacy behaviour. Set false in hosts that already render their
|
|
27
|
+
* own (shared) `<DebugBar />` to avoid two bars on screen.
|
|
28
|
+
*/
|
|
29
|
+
debugBar?: boolean
|
|
30
|
+
}>(),
|
|
31
|
+
{ debugBar: true },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Read the singleton-on-window debug bar ref. Using a getter (instead
|
|
35
|
+
// of importing the module-level const) makes this resilient to Vite
|
|
36
|
+
// HMR's module fragmentation in dev, where a top-level `const ref`
|
|
37
|
+
// can be torn into separate instances per importer.
|
|
38
|
+
const debugBarRef = getQdadmDebugBarRef()
|
|
39
|
+
const debugBarComp = computed<Component | null>(() =>
|
|
40
|
+
props.debugBar ? debugBarRef.value : null,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// `qdadmHasPrimeVue` is provided by the Kernel during _installPlugins.
|
|
44
|
+
// True when PrimeVue is wired (so Toast / ToastListener are usable).
|
|
45
|
+
const hasToast = inject<boolean>('qdadmHasPrimeVue', false)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<Toast v-if="hasToast" />
|
|
50
|
+
<ToastListener v-if="hasToast" />
|
|
51
|
+
<component v-if="debugBarComp" :is="debugBarComp" />
|
|
52
|
+
</template>
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import Button from 'primevue/button'
|
|
17
|
+
import { useI18n } from '../../i18n/useI18n'
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n()
|
|
17
20
|
|
|
18
21
|
interface Props {
|
|
19
22
|
isEdit?: boolean
|
|
@@ -41,31 +44,31 @@ const emit = defineEmits<{
|
|
|
41
44
|
<div class="form-actions-left">
|
|
42
45
|
<Button
|
|
43
46
|
type="button"
|
|
44
|
-
:label="isEdit ? '
|
|
47
|
+
:label="isEdit ? t('core.actions.update') : t('core.actions.create')"
|
|
45
48
|
:loading="saving"
|
|
46
49
|
:disabled="!dirty || saving"
|
|
47
50
|
icon="pi pi-check"
|
|
48
51
|
@click="emit('save')"
|
|
49
|
-
v-tooltip.top="'
|
|
52
|
+
v-tooltip.top="t('core.tooltips.saveAndContinue')"
|
|
50
53
|
/>
|
|
51
54
|
<Button
|
|
52
55
|
v-if="showSaveAndClose"
|
|
53
56
|
type="button"
|
|
54
|
-
:label="isEdit ? '
|
|
57
|
+
:label="isEdit ? t('core.actions.updateAndClose') : t('core.actions.createAndClose')"
|
|
55
58
|
:loading="saving"
|
|
56
59
|
:disabled="!dirty || saving"
|
|
57
60
|
icon="pi pi-check-circle"
|
|
58
61
|
severity="success"
|
|
59
62
|
@click="emit('saveAndClose')"
|
|
60
|
-
v-tooltip.top="'
|
|
63
|
+
v-tooltip.top="t('core.tooltips.saveAndReturn')"
|
|
61
64
|
/>
|
|
62
|
-
<span v-if="dirty" class="dirty-indicator" v-tooltip.top="'
|
|
65
|
+
<span v-if="dirty" class="dirty-indicator" v-tooltip.top="t('core.tooltips.unsavedChanges')">
|
|
63
66
|
<i class="pi pi-circle-fill"></i>
|
|
64
67
|
</span>
|
|
65
68
|
</div>
|
|
66
69
|
<Button
|
|
67
70
|
type="button"
|
|
68
|
-
label="
|
|
71
|
+
:label="t('core.actions.cancel')"
|
|
69
72
|
severity="secondary"
|
|
70
73
|
icon="pi pi-times"
|
|
71
74
|
@click="emit('cancel')"
|
|
@@ -13,6 +13,9 @@ import DataTable from 'primevue/datatable'
|
|
|
13
13
|
import Column from 'primevue/column'
|
|
14
14
|
import InputText from 'primevue/inputtext'
|
|
15
15
|
import Button from 'primevue/button'
|
|
16
|
+
import { useI18n } from '../../i18n/useI18n'
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n()
|
|
16
19
|
|
|
17
20
|
export interface LookupColumn {
|
|
18
21
|
field: string
|
|
@@ -163,7 +166,7 @@ const confirmLabel = computed(() => {
|
|
|
163
166
|
<i class="pi pi-search" />
|
|
164
167
|
<InputText
|
|
165
168
|
v-model="searchQuery"
|
|
166
|
-
placeholder="
|
|
169
|
+
:placeholder="t('core.placeholders.search')"
|
|
167
170
|
class="w-full"
|
|
168
171
|
autofocus
|
|
169
172
|
/>
|
|
@@ -199,14 +202,14 @@ const confirmLabel = computed(() => {
|
|
|
199
202
|
/>
|
|
200
203
|
<template #empty>
|
|
201
204
|
<div class="text-center text-color-secondary py-4">
|
|
202
|
-
{{ searchQuery ? '
|
|
205
|
+
{{ searchQuery ? t('core.messages.noMatching') : t('core.messages.empty') }}
|
|
203
206
|
</div>
|
|
204
207
|
</template>
|
|
205
208
|
</DataTable>
|
|
206
209
|
|
|
207
210
|
<template #footer>
|
|
208
211
|
<Button
|
|
209
|
-
label="
|
|
212
|
+
:label="t('core.actions.cancel')"
|
|
210
213
|
severity="secondary"
|
|
211
214
|
@click="onCancel"
|
|
212
215
|
/>
|
package/src/components/index.ts
CHANGED
|
@@ -75,3 +75,9 @@ export { default as BannerZone } from './display/BannerZone.vue'
|
|
|
75
75
|
// Pages
|
|
76
76
|
export { default as LoginPage } from './pages/LoginPage.vue'
|
|
77
77
|
export { default as NotFoundPage } from './pages/NotFoundPage.vue'
|
|
78
|
+
|
|
79
|
+
// Host integration helper — render in a host App.vue when using
|
|
80
|
+
// `Kernel({ existingApp })` so qdadm's DOM extras (Toast,
|
|
81
|
+
// ToastListener, DebugBar) appear without the Kernel-managed
|
|
82
|
+
// WrappedRoot.
|
|
83
|
+
export { default as QdadmRoot } from './QdadmRoot.vue'
|
|
@@ -501,6 +501,7 @@ export function useEntityItemFormPage<T extends Record<string, unknown> = Record
|
|
|
501
501
|
const fieldManager = useFieldManager<ResolvedFieldConfig>({
|
|
502
502
|
resolveFieldConfig,
|
|
503
503
|
getSchemaFieldConfig: (name) => manager.getFieldConfig(name) || null,
|
|
504
|
+
entity,
|
|
504
505
|
})
|
|
505
506
|
|
|
506
507
|
// Expose refs for validation (which needs direct access)
|
|
@@ -431,6 +431,7 @@ export function useEntityItemShowPage<T = unknown>(
|
|
|
431
431
|
const fieldManager = useFieldManager<ResolvedFieldConfig>({
|
|
432
432
|
resolveFieldConfig,
|
|
433
433
|
getSchemaFieldConfig: (name) => manager.getFieldConfig?.(name) || null,
|
|
434
|
+
entity,
|
|
434
435
|
})
|
|
435
436
|
|
|
436
437
|
// Direct access to computed values
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* fieldManager.group('info', ['name', 'email'], { label: 'Information' })
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
21
|
+
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
|
22
|
+
import { useI18n } from '../i18n/useI18n'
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Base field definition (minimal common interface)
|
|
@@ -155,6 +156,15 @@ export interface UseFieldManagerOptions<T extends ResolvedFieldConfig = Resolved
|
|
|
155
156
|
resolveFieldConfig?: FieldResolver<T>
|
|
156
157
|
/** Get field config from schema (e.g., manager.getFieldConfig) */
|
|
157
158
|
getSchemaFieldConfig?: (name: string) => Partial<BaseFieldDefinition> | null
|
|
159
|
+
/**
|
|
160
|
+
* Entity name. When set, field and group labels are resolved through
|
|
161
|
+
* `entities.{entity}.fields.{name}` / `entities.{entity}.groups.{name}` via
|
|
162
|
+
* i18n. The result wins over inline `label:` props if found; otherwise the
|
|
163
|
+
* inline label (then `snakeCaseToTitle`) is used. Required for the i18n
|
|
164
|
+
* forcing function — modules without an entity context fall back to legacy
|
|
165
|
+
* behaviour.
|
|
166
|
+
*/
|
|
167
|
+
entity?: string
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
/**
|
|
@@ -189,6 +199,13 @@ export interface UseFieldManagerReturn<T extends ResolvedFieldConfig = ResolvedF
|
|
|
189
199
|
hasField: (name: string) => boolean
|
|
190
200
|
hasGroup: (name: string) => boolean
|
|
191
201
|
getUngroupedFields: () => T[]
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Re-resolve labels for all fields and groups against the active locale.
|
|
205
|
+
* Called automatically when `i18n.locale` changes; exposed for manual
|
|
206
|
+
* triggering after asynchronous bundle loads.
|
|
207
|
+
*/
|
|
208
|
+
relabel: () => void
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
/**
|
|
@@ -224,10 +241,41 @@ export function useFieldManager<T extends ResolvedFieldConfig = ResolvedFieldCon
|
|
|
224
241
|
options: UseFieldManagerOptions<T> = {}
|
|
225
242
|
): UseFieldManagerReturn<T> {
|
|
226
243
|
const {
|
|
227
|
-
resolveFieldConfig = defaultResolveFieldConfig as FieldResolver<T>,
|
|
244
|
+
resolveFieldConfig: userResolveFieldConfig = defaultResolveFieldConfig as FieldResolver<T>,
|
|
228
245
|
getSchemaFieldConfig,
|
|
246
|
+
entity: entityName,
|
|
229
247
|
} = options
|
|
230
248
|
|
|
249
|
+
// i18n integration — looked up from the kernel injection. Returns a no-op
|
|
250
|
+
// shim if no kernel i18n is available (tests, isolated examples).
|
|
251
|
+
const { i18n, locale: i18nLocale } = useI18n()
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Wrapped resolver: runs the user's resolver, then overlays the i18n label
|
|
255
|
+
* if the convention key resolves to a real translation. Tags the resolved
|
|
256
|
+
* config with `_labelKey` so relabel() can re-resolve on locale change.
|
|
257
|
+
*/
|
|
258
|
+
function resolveFieldConfig(
|
|
259
|
+
name: string,
|
|
260
|
+
fieldConfig: Partial<BaseFieldDefinition>
|
|
261
|
+
): T {
|
|
262
|
+
const resolved = userResolveFieldConfig(name, fieldConfig) as T & {
|
|
263
|
+
_labelKey?: string
|
|
264
|
+
_inlineLabel?: string
|
|
265
|
+
}
|
|
266
|
+
if (!entityName || !i18n) return resolved
|
|
267
|
+
const key = `entities.${entityName}.fields.${name}`
|
|
268
|
+
resolved._labelKey = key
|
|
269
|
+
// Preserve the inline label (if the user supplied one) so relabel() can
|
|
270
|
+
// fall back to it when the i18n bundle has no matching key.
|
|
271
|
+
resolved._inlineLabel = fieldConfig.label as string | undefined
|
|
272
|
+
const trace = i18n.resolve(key)
|
|
273
|
+
if (trace.hit) {
|
|
274
|
+
resolved.label = trace.result
|
|
275
|
+
}
|
|
276
|
+
return resolved
|
|
277
|
+
}
|
|
278
|
+
|
|
231
279
|
// ============ FIELD STORAGE ============
|
|
232
280
|
|
|
233
281
|
const fieldsMap = ref<Map<string, T>>(new Map()) as Ref<Map<string, T>>
|
|
@@ -405,9 +453,17 @@ export function useFieldManager<T extends ResolvedFieldConfig = ResolvedFieldCon
|
|
|
405
453
|
|
|
406
454
|
// Create or update group
|
|
407
455
|
const existing = groupsMap.value.get(name)
|
|
456
|
+
// Resolve label: i18n hit (entities.X.groups.{name}) wins; fall back to
|
|
457
|
+
// explicit option, then existing label, then humanized form.
|
|
458
|
+
let resolvedLabel =
|
|
459
|
+
groupOptions?.label || existing?.label || snakeCaseToTitle(groupName)
|
|
460
|
+
if (entityName && i18n) {
|
|
461
|
+
const trace = i18n.resolve(`entities.${entityName}.groups.${name}`)
|
|
462
|
+
if (trace.hit) resolvedLabel = trace.result
|
|
463
|
+
}
|
|
408
464
|
groupsMap.value.set(name, {
|
|
409
465
|
name,
|
|
410
|
-
label:
|
|
466
|
+
label: resolvedLabel,
|
|
411
467
|
fields: [...(existing?.fields || []), ...fieldNames],
|
|
412
468
|
parent: parentName,
|
|
413
469
|
// Tab/accordion options (preserve existing if not specified)
|
|
@@ -558,6 +614,44 @@ export function useFieldManager<T extends ResolvedFieldConfig = ResolvedFieldCon
|
|
|
558
614
|
return rootGroups
|
|
559
615
|
})
|
|
560
616
|
|
|
617
|
+
// ============ I18N RELABEL ============
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Re-resolve labels for all fields and groups against the active locale.
|
|
621
|
+
* Iterates the existing maps and replaces each entry's label using the
|
|
622
|
+
* stored convention key, falling back to the inline label or
|
|
623
|
+
* snakeCaseToTitle when the key has no translation.
|
|
624
|
+
*/
|
|
625
|
+
function relabel(): void {
|
|
626
|
+
if (!entityName || !i18n) return
|
|
627
|
+
|
|
628
|
+
for (const [name, field] of fieldsMap.value.entries()) {
|
|
629
|
+
const meta = field as T & { _labelKey?: string; _inlineLabel?: string }
|
|
630
|
+
const key = meta._labelKey ?? `entities.${entityName}.fields.${name}`
|
|
631
|
+
const trace = i18n.resolve(key)
|
|
632
|
+
const newLabel = trace.hit
|
|
633
|
+
? trace.result
|
|
634
|
+
: meta._inlineLabel || snakeCaseToTitle(name)
|
|
635
|
+
if (field.label !== newLabel) {
|
|
636
|
+
fieldsMap.value.set(name, { ...field, label: newLabel } as T)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (const [name, internal] of groupsMap.value.entries()) {
|
|
641
|
+
const key = `entities.${entityName}.groups.${name}`
|
|
642
|
+
const trace = i18n.resolve(key)
|
|
643
|
+
if (trace.hit && internal.label !== trace.result) {
|
|
644
|
+
groupsMap.value.set(name, { ...internal, label: trace.result })
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (entityName && i18n) {
|
|
650
|
+
// Auto-relabel on locale change. Vue's watcher tracks the ref returned
|
|
651
|
+
// from useI18n(), so this fires for both immediate and async switches.
|
|
652
|
+
watch(i18nLocale, () => relabel())
|
|
653
|
+
}
|
|
654
|
+
|
|
561
655
|
// ============ RETURN ============
|
|
562
656
|
|
|
563
657
|
const returnValue: UseFieldManagerReturn<T> = {
|
|
@@ -589,6 +683,9 @@ export function useFieldManager<T extends ResolvedFieldConfig = ResolvedFieldCon
|
|
|
589
683
|
hasField,
|
|
590
684
|
hasGroup,
|
|
591
685
|
getUngroupedFields,
|
|
686
|
+
|
|
687
|
+
// i18n
|
|
688
|
+
relabel,
|
|
592
689
|
}
|
|
593
690
|
|
|
594
691
|
return returnValue
|