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.
- package/package.json +8 -4
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with i18n declarative methods:
|
|
3
|
+
* - messages(locale, bundle) — declare translations
|
|
4
|
+
* - aliases(patterns) — declare pattern aliases
|
|
5
|
+
* - messagesProvider(provider) — register a TranslationProvider
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AliasPattern, MessagesBundle, TranslationProvider } from '../i18n/types'
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type Self = any
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
export function applyI18nMethods(KernelContextClass: { prototype: any }): void {
|
|
15
|
+
const proto = KernelContextClass.prototype as Self
|
|
16
|
+
|
|
17
|
+
proto.messages = function (this: Self, locale: string, bundle: MessagesBundle): Self {
|
|
18
|
+
if (this._kernel.i18nInstance) {
|
|
19
|
+
this._kernel.i18nInstance.addMessages(locale, bundle)
|
|
20
|
+
}
|
|
21
|
+
return this
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
proto.aliases = function (this: Self, patterns: AliasPattern[]): Self {
|
|
25
|
+
if (this._kernel.i18nInstance) {
|
|
26
|
+
this._kernel.i18nInstance.addAliases(patterns)
|
|
27
|
+
}
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
proto.messagesProvider = function (this: Self, provider: TranslationProvider): Self {
|
|
32
|
+
if (this._kernel.i18nInstance) {
|
|
33
|
+
this._kernel.i18nInstance.addProvider(provider)
|
|
34
|
+
}
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with permission registration methods:
|
|
3
|
+
* - permissions(namespace, perms, options) — generic registration
|
|
4
|
+
* - entityPermissions(entity, perms) — shorthand for entity-scoped
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PermissionMeta, PermissionOptions } from './KernelContext.types'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type Self = any
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
export function applyPermissionMethods(KernelContextClass: { prototype: any }): void {
|
|
14
|
+
const proto = KernelContextClass.prototype as Self
|
|
15
|
+
|
|
16
|
+
proto.permissions = function (
|
|
17
|
+
this: Self,
|
|
18
|
+
namespace: string,
|
|
19
|
+
permissions: Record<string, string | PermissionMeta>,
|
|
20
|
+
options: PermissionOptions = {}
|
|
21
|
+
): Self {
|
|
22
|
+
if (this._kernel.permissionRegistry) {
|
|
23
|
+
this._kernel.permissionRegistry.register(namespace, permissions, {
|
|
24
|
+
...options,
|
|
25
|
+
module: (this._module?.constructor as { name?: string })?.name || 'unknown',
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
proto.entityPermissions = function (
|
|
32
|
+
this: Self,
|
|
33
|
+
entity: string,
|
|
34
|
+
permissions: Record<string, string | PermissionMeta>
|
|
35
|
+
): Self {
|
|
36
|
+
return this.permissions(entity, permissions, { isEntity: true })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch KernelContext prototype with routing-related methods:
|
|
3
|
+
* - routes(basePath, routes, opts)
|
|
4
|
+
* - navItem(item)
|
|
5
|
+
* - routeFamily(base, prefixes)
|
|
6
|
+
* - crud(entity, pages, options) — convention CRUD generator
|
|
7
|
+
* - childPage(parent, page, options) — non-entity child route
|
|
8
|
+
*
|
|
9
|
+
* The string helpers (`singularize`, `toKebab`, `capitalize`) are kept
|
|
10
|
+
* module-local — they're pure, stateless, and were only used by `crud()` and
|
|
11
|
+
* `childPage()`. Keeping them off the prototype keeps the public shape clean.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
15
|
+
import { registry, getRoutes } from '../module/moduleRegistry'
|
|
16
|
+
import type {
|
|
17
|
+
ChildPageOptions,
|
|
18
|
+
CrudOptions,
|
|
19
|
+
CrudPages,
|
|
20
|
+
NavItem,
|
|
21
|
+
ParentConfig,
|
|
22
|
+
RouteOptions,
|
|
23
|
+
} from './KernelContext.types'
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
type Self = any
|
|
27
|
+
|
|
28
|
+
/** Singularize a plural word (simple English rules) */
|
|
29
|
+
function singularize(plural: string): string {
|
|
30
|
+
if (plural.endsWith('ies')) return plural.slice(0, -3) + 'y'
|
|
31
|
+
if (plural.endsWith('ses') || plural.endsWith('xes') || plural.endsWith('zes')) {
|
|
32
|
+
return plural.slice(0, -2)
|
|
33
|
+
}
|
|
34
|
+
if (plural.endsWith('s') && !plural.endsWith('ss')) return plural.slice(0, -1)
|
|
35
|
+
return plural
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convert camelCase to kebab-case (e.g. 'botTasks' → 'bot-tasks') */
|
|
39
|
+
function toKebab(str: string): string {
|
|
40
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Capitalize first letter */
|
|
44
|
+
function capitalize(str: string): string {
|
|
45
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
export function applyRoutingMethods(KernelContextClass: { prototype: any }): void {
|
|
50
|
+
const proto = KernelContextClass.prototype as Self
|
|
51
|
+
|
|
52
|
+
proto.routes = function (
|
|
53
|
+
this: Self,
|
|
54
|
+
basePath: string,
|
|
55
|
+
routes: RouteRecordRaw[],
|
|
56
|
+
opts: RouteOptions = {}
|
|
57
|
+
): Self {
|
|
58
|
+
registry.addRoutes(basePath, routes, opts)
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
proto.navItem = function (this: Self, item: NavItem): Self {
|
|
63
|
+
registry.addNavItem(item)
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
proto.routeFamily = function (this: Self, base: string, prefixes: string[]): Self {
|
|
68
|
+
registry.addRouteFamily(base, prefixes)
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register standard CRUD routes with naming conventions
|
|
74
|
+
* (entity 'books' → route prefix 'book', /books → 'book', etc.)
|
|
75
|
+
*/
|
|
76
|
+
proto.crud = function (
|
|
77
|
+
this: Self,
|
|
78
|
+
entity: string,
|
|
79
|
+
pages: CrudPages,
|
|
80
|
+
options: CrudOptions = {}
|
|
81
|
+
): Self {
|
|
82
|
+
// Entity name is always used for permission binding.
|
|
83
|
+
// Manager may not be registered yet (child entity before parent module loads).
|
|
84
|
+
const entityBinding = entity
|
|
85
|
+
const manager = this._kernel.orchestrator?.isRegistered(entity)
|
|
86
|
+
? this._kernel.orchestrator.get(entity)
|
|
87
|
+
: null
|
|
88
|
+
const idParam = manager?.idField || 'id'
|
|
89
|
+
|
|
90
|
+
// Handle parent route configuration
|
|
91
|
+
const urlSegment = options.pathSegment || toKebab(entity)
|
|
92
|
+
let basePath = urlSegment
|
|
93
|
+
let parentConfig: ParentConfig | null = null
|
|
94
|
+
let parentRoutePrefix: string | null = null
|
|
95
|
+
|
|
96
|
+
if (options.parentRoute) {
|
|
97
|
+
const parentRouteName = options.parentRoute
|
|
98
|
+
const allRoutes = getRoutes()
|
|
99
|
+
const parentRoute = allRoutes.find((r) => r.name === parentRouteName)
|
|
100
|
+
|
|
101
|
+
if (parentRoute) {
|
|
102
|
+
const parentEntityName = parentRoute.meta?.entity
|
|
103
|
+
const parentManager = parentEntityName
|
|
104
|
+
? this._kernel.orchestrator?.get(parentEntityName)
|
|
105
|
+
: null
|
|
106
|
+
const parentIdParam = parentManager?.idField || 'id'
|
|
107
|
+
|
|
108
|
+
// Build base path: parentPath/:parentId/entity (e.g., books/:bookId/loans)
|
|
109
|
+
const parentBasePath =
|
|
110
|
+
parentRoute.path.replace(/\/(create|:.*)?$/, '') || parentEntityName
|
|
111
|
+
basePath = `${parentBasePath}/:${parentIdParam}/${urlSegment}`
|
|
112
|
+
|
|
113
|
+
if (parentEntityName) {
|
|
114
|
+
parentConfig = {
|
|
115
|
+
entity: parentEntityName,
|
|
116
|
+
param: parentIdParam,
|
|
117
|
+
foreignKey: options.foreignKey || `${singularize(parentEntityName)}_id`,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
parentRoutePrefix = parentRouteName
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Derive route prefix:
|
|
126
|
+
// With parent: 'book' + '-loan' → 'book-loan'
|
|
127
|
+
// Without parent: 'books' → 'book'
|
|
128
|
+
const routePrefix =
|
|
129
|
+
options.routePrefix ||
|
|
130
|
+
(parentRoutePrefix
|
|
131
|
+
? `${parentRoutePrefix}-${singularize(entity)}`
|
|
132
|
+
: singularize(entity))
|
|
133
|
+
|
|
134
|
+
const routes: RouteRecordRaw[] = []
|
|
135
|
+
|
|
136
|
+
if (pages.list) {
|
|
137
|
+
routes.push({
|
|
138
|
+
path: '',
|
|
139
|
+
name: routePrefix,
|
|
140
|
+
component: pages.list,
|
|
141
|
+
meta: { layout: 'list' },
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (pages.show) {
|
|
146
|
+
routes.push({
|
|
147
|
+
path: `:${idParam}`,
|
|
148
|
+
name: `${routePrefix}-show`,
|
|
149
|
+
component: pages.show,
|
|
150
|
+
meta: { layout: 'show' },
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (pages.form) {
|
|
155
|
+
// Single form pattern (recommended)
|
|
156
|
+
routes.push({
|
|
157
|
+
path: 'create',
|
|
158
|
+
name: `${routePrefix}-create`,
|
|
159
|
+
component: pages.form,
|
|
160
|
+
})
|
|
161
|
+
routes.push({
|
|
162
|
+
path: `:${idParam}/edit`,
|
|
163
|
+
name: `${routePrefix}-edit`,
|
|
164
|
+
component: pages.form,
|
|
165
|
+
})
|
|
166
|
+
} else {
|
|
167
|
+
if (pages.create) {
|
|
168
|
+
routes.push({
|
|
169
|
+
path: 'create',
|
|
170
|
+
name: `${routePrefix}-create`,
|
|
171
|
+
component: pages.create,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
if (pages.edit) {
|
|
175
|
+
routes.push({
|
|
176
|
+
path: `:${idParam}/edit`,
|
|
177
|
+
name: `${routePrefix}-edit`,
|
|
178
|
+
component: pages.edit,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const routeOpts: RouteOptions = {}
|
|
184
|
+
// Set entity if registered, OR if this is a child route — child routes
|
|
185
|
+
// need entity binding for permission checks even before the manager
|
|
186
|
+
// exists.
|
|
187
|
+
if (manager || parentConfig) {
|
|
188
|
+
routeOpts.entity = entityBinding
|
|
189
|
+
}
|
|
190
|
+
if (parentConfig) {
|
|
191
|
+
routeOpts.parent = parentConfig
|
|
192
|
+
}
|
|
193
|
+
if (options.label) {
|
|
194
|
+
routeOpts.label = options.label
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.routes(basePath, routes, routeOpts)
|
|
198
|
+
this.routeFamily(routePrefix, [`${routePrefix}-`])
|
|
199
|
+
|
|
200
|
+
if (options.nav) {
|
|
201
|
+
const label = options.nav.label || capitalize(entity)
|
|
202
|
+
const navItem: NavItem = {
|
|
203
|
+
section: options.nav.section,
|
|
204
|
+
route: routePrefix,
|
|
205
|
+
icon: options.nav.icon,
|
|
206
|
+
label,
|
|
207
|
+
}
|
|
208
|
+
// Only set entity on nav item if registered (avoids permission check
|
|
209
|
+
// failure). Routes always get entity binding, but nav items need it
|
|
210
|
+
// resolvable.
|
|
211
|
+
if (manager) {
|
|
212
|
+
navItem.entity = entityBinding
|
|
213
|
+
}
|
|
214
|
+
this.navItem(navItem)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Register a custom child page on an entity item (e.g. /books/:id/statistics).
|
|
222
|
+
*/
|
|
223
|
+
proto.childPage = function (
|
|
224
|
+
this: Self,
|
|
225
|
+
parentRouteName: string,
|
|
226
|
+
pageName: string,
|
|
227
|
+
options: ChildPageOptions
|
|
228
|
+
): Self {
|
|
229
|
+
const allRoutes = getRoutes()
|
|
230
|
+
const parentRoute = allRoutes.find((r) => r.name === parentRouteName)
|
|
231
|
+
|
|
232
|
+
if (!parentRoute) {
|
|
233
|
+
console.warn(`[qdadm] childPage: parent route '${parentRouteName}' not found`)
|
|
234
|
+
return this
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const parentEntityName = parentRoute.meta?.entity as string | undefined
|
|
238
|
+
const parentManager = parentEntityName
|
|
239
|
+
? this._kernel.orchestrator?.get(parentEntityName)
|
|
240
|
+
: null
|
|
241
|
+
const parentIdParam = parentManager?.idField || 'id'
|
|
242
|
+
|
|
243
|
+
const parentBasePath =
|
|
244
|
+
parentRoute.path.replace(/\/(create|:.*)?$/, '') || parentEntityName
|
|
245
|
+
const basePath = `${parentBasePath}/:${parentIdParam}/${pageName}`
|
|
246
|
+
|
|
247
|
+
const routeName = `${parentRouteName}-${pageName}`
|
|
248
|
+
|
|
249
|
+
const parentConfig: ParentConfig | undefined = parentEntityName
|
|
250
|
+
? { entity: parentEntityName, param: parentIdParam }
|
|
251
|
+
: undefined
|
|
252
|
+
|
|
253
|
+
const routeOpts: RouteOptions = {}
|
|
254
|
+
if (parentConfig) {
|
|
255
|
+
routeOpts.parent = parentConfig
|
|
256
|
+
}
|
|
257
|
+
if (options.label) {
|
|
258
|
+
routeOpts.label = options.label
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.routes(
|
|
262
|
+
basePath,
|
|
263
|
+
[
|
|
264
|
+
{
|
|
265
|
+
path: '',
|
|
266
|
+
name: routeName,
|
|
267
|
+
component: options.component,
|
|
268
|
+
meta: {
|
|
269
|
+
layout: 'page',
|
|
270
|
+
...(options.icon && { icon: options.icon }),
|
|
271
|
+
...options.meta,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
routeOpts
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return this
|
|
279
|
+
}
|
|
280
|
+
}
|