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
@@ -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
+ }