@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,251 @@
1
+ /**
2
+ * qdadm Vue Plugin
3
+ *
4
+ * Creates and configures the qdadm framework for Vue applications.
5
+ */
6
+
7
+ import type { App } from 'vue'
8
+ import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
9
+ import { Orchestrator } from './orchestrator/Orchestrator'
10
+ import { EntityManager } from './entity/EntityManager'
11
+ import qdadmLogo from './assets/logo.svg'
12
+
13
+ /**
14
+ * Toast service interface
15
+ */
16
+ export interface ToastService {
17
+ add: (options: {
18
+ severity: string
19
+ summary: string
20
+ detail: string
21
+ life?: number
22
+ }) => void
23
+ }
24
+
25
+ /**
26
+ * Auth adapter interface
27
+ */
28
+ export interface AuthAdapter {
29
+ login?: (credentials: unknown) => Promise<unknown>
30
+ logout?: () => void | Promise<void>
31
+ isAuthenticated?: () => boolean
32
+ getUser?: () => unknown
33
+ [key: string]: unknown
34
+ }
35
+
36
+ /**
37
+ * App theme configuration
38
+ */
39
+ export interface AppTheme {
40
+ primary?: string
41
+ surface?: string
42
+ [key: string]: string | undefined
43
+ }
44
+
45
+ /**
46
+ * App configuration
47
+ */
48
+ export interface AppConfig {
49
+ name?: string
50
+ shortName?: string
51
+ logo?: string | null
52
+ logoSmall?: string | null
53
+ favicon?: string | null
54
+ version?: string | null
55
+ theme?: AppTheme
56
+ [key: string]: unknown
57
+ }
58
+
59
+ /**
60
+ * Features configuration
61
+ */
62
+ export interface FeaturesConfig {
63
+ auth?: boolean
64
+ poweredBy?: boolean
65
+ breadcrumb?: boolean
66
+ [key: string]: boolean | undefined
67
+ }
68
+
69
+ /**
70
+ * Module system configuration
71
+ */
72
+ export interface ModulesConfig {
73
+ sectionOrder?: string[]
74
+ [key: string]: unknown
75
+ }
76
+
77
+ /**
78
+ * qdadm plugin options
79
+ */
80
+ export interface QdadmOptions {
81
+ entityFactory?: (entityName: string, entityConfig: unknown) => EntityManager
82
+ managers?: Record<string, EntityManager>
83
+ orchestrator?: Orchestrator
84
+ authAdapter?: AuthAdapter
85
+ router: Router
86
+ toast: ToastService
87
+ app?: AppConfig
88
+ modules?: ModulesConfig
89
+ features?: FeaturesConfig
90
+ builtinModules?: Record<string, boolean>
91
+ endpoints?: Record<string, string>
92
+ homeRoute?: string
93
+ }
94
+
95
+ /**
96
+ * Vue plugin interface
97
+ */
98
+ export interface QdadmPlugin {
99
+ install: (app: App) => void
100
+ }
101
+
102
+ /**
103
+ * Creates the qdadm plugin with the given configuration
104
+ */
105
+ export function createQdadm(options: QdadmOptions): QdadmPlugin {
106
+ return {
107
+ install(app: App) {
108
+ // Validation des contrats requis
109
+ if (!options.entityFactory && !options.orchestrator && !options.managers) {
110
+ throw new Error('[qdadm] entityFactory, orchestrator, or managers required')
111
+ }
112
+ if (!options.router) {
113
+ throw new Error('[qdadm] router required')
114
+ }
115
+ if (!options.toast) {
116
+ throw new Error('[qdadm] toast required')
117
+ }
118
+
119
+ // Create or use provided Orchestrator
120
+ let orchestrator = options.orchestrator
121
+ if (!orchestrator) {
122
+ orchestrator = new Orchestrator({
123
+ entityFactory: options.entityFactory,
124
+ managers: options.managers || {}
125
+ })
126
+ } else if (options.managers) {
127
+ // Register additional managers on existing orchestrator
128
+ for (const [name, manager] of Object.entries(options.managers)) {
129
+ orchestrator.register(name, manager)
130
+ }
131
+ }
132
+
133
+ // Features config avec défauts
134
+ const features: FeaturesConfig = {
135
+ auth: true,
136
+ poweredBy: true,
137
+ breadcrumb: true,
138
+ ...options.features
139
+ }
140
+
141
+ // App config avec défauts
142
+ const appConfig: AppConfig = {
143
+ name: 'Admin',
144
+ shortName: 'Admin',
145
+ logo: null,
146
+ logoSmall: null,
147
+ favicon: qdadmLogo,
148
+ version: null,
149
+ theme: {
150
+ primary: '#3B82F6',
151
+ surface: '#1E293B',
152
+ ...options.app?.theme
153
+ },
154
+ ...options.app
155
+ }
156
+
157
+ // Endpoints config
158
+ const endpoints: Record<string, string> = {
159
+ users: '/users',
160
+ roles: '/roles',
161
+ ...options.endpoints
162
+ }
163
+
164
+ // Builtin modules config
165
+ const builtinModules: Record<string, boolean> = {
166
+ users: features.auth || false,
167
+ roles: features.auth || false,
168
+ ...options.builtinModules
169
+ }
170
+
171
+ // Validation conditionnelle
172
+ if (features.auth && !options.authAdapter) {
173
+ throw new Error('[qdadm] authAdapter required when features.auth is true')
174
+ }
175
+
176
+ // Provide all dependencies
177
+ app.provide('qdadmOrchestrator', orchestrator)
178
+ app.provide('qdadmRouter', options.router)
179
+ app.provide('qdadmToast', options.toast)
180
+ app.provide('qdadmFeatures', features)
181
+ app.provide('qdadmApp', appConfig)
182
+ app.provide('qdadmEndpoints', endpoints)
183
+ app.provide('qdadmBuiltinModules', builtinModules)
184
+
185
+ if (options.authAdapter) {
186
+ app.provide('authAdapter', options.authAdapter)
187
+ }
188
+
189
+ if (options.modules?.sectionOrder) {
190
+ app.provide('qdadmSectionOrder', options.modules.sectionOrder)
191
+ }
192
+
193
+ if (options.homeRoute) {
194
+ app.provide('qdadmHomeRoute', options.homeRoute)
195
+ }
196
+
197
+ // Add route guard for entity permissions
198
+ options.router.beforeEach((
199
+ to: RouteLocationNormalized,
200
+ _from: RouteLocationNormalized,
201
+ next: NavigationGuardNext
202
+ ) => {
203
+ const entity = to.meta?.entity as string | undefined
204
+ if (entity && orchestrator) {
205
+ const manager = orchestrator.get(entity) as EntityManager & { canRead?: () => boolean }
206
+ if (manager && manager.canRead && !manager.canRead()) {
207
+ // Show visible error via toast
208
+ console.warn(`[qdadm] Access denied to ${to.path} (entity: ${entity})`)
209
+ options.toast.add({
210
+ severity: 'error',
211
+ summary: 'Access Denied',
212
+ detail: `You don't have permission to access ${manager.labelPlural || entity}`,
213
+ life: 5000
214
+ })
215
+ // Emit signal if orchestrator has signals configured
216
+ if (orchestrator.signals) {
217
+ orchestrator.signals.emit('auth:access-denied', {
218
+ path: to.path,
219
+ entity,
220
+ manager
221
+ })
222
+ }
223
+ return next({ path: '/' })
224
+ }
225
+ }
226
+ next()
227
+ })
228
+
229
+ // Appliquer le thème via CSS variables
230
+ if (appConfig.theme) {
231
+ if (appConfig.theme.primary) {
232
+ document.documentElement.style.setProperty('--qdadm-primary', appConfig.theme.primary)
233
+ }
234
+ if (appConfig.theme.surface) {
235
+ document.documentElement.style.setProperty('--qdadm-surface', appConfig.theme.surface)
236
+ }
237
+ }
238
+
239
+ // Appliquer le favicon
240
+ if (appConfig.favicon) {
241
+ const existingLink = document.querySelector("link[rel~='icon']")
242
+ const link = existingLink || document.createElement('link')
243
+ link.setAttribute('rel', 'icon')
244
+ link.setAttribute('href', appConfig.favicon)
245
+ if (!existingLink) {
246
+ document.head.appendChild(link)
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * FilterQuery - Resolves dropdown options for filters
3
+ *
4
+ * Abstracts option resolution from two sources:
5
+ * - 'entity': fetch options from a related EntityManager
6
+ * - 'field': extract distinct values from parent manager's cached items
7
+ *
8
+ * Usage:
9
+ * ```js
10
+ * // Entity source - fetch from genres EntityManager
11
+ * const query = new FilterQuery({
12
+ * source: 'entity',
13
+ * entity: 'genres',
14
+ * label: 'name',
15
+ * value: 'id'
16
+ * })
17
+ *
18
+ * // Field source - extract unique status values
19
+ * const query = new FilterQuery({
20
+ * source: 'field',
21
+ * field: 'status'
22
+ * })
23
+ *
24
+ * // Get options (async)
25
+ * const options = await query.getOptions(orchestrator)
26
+ * // → [{ label: 'Rock', value: 1 }, { label: 'Jazz', value: 2 }]
27
+ * ```
28
+ */
29
+ import { getNestedValue } from './QueryExecutor'
30
+
31
+ const VALID_SOURCES = ['entity', 'field'] as const
32
+
33
+ export type FilterQuerySource = (typeof VALID_SOURCES)[number]
34
+
35
+ /**
36
+ * Option item for dropdowns
37
+ */
38
+ export interface FilterOption {
39
+ label: string | null | undefined
40
+ value: unknown
41
+ }
42
+
43
+ /**
44
+ * Value resolver - can be a path string or function
45
+ */
46
+ export type ValueResolver<T = unknown> = string | ((item: T) => unknown)
47
+
48
+ /**
49
+ * FilterQuery constructor options
50
+ */
51
+ export interface FilterQueryOptions<T = unknown> {
52
+ source: FilterQuerySource
53
+ entity?: string | null
54
+ field?: string | null
55
+ parentManager?: EntityManagerLike | null
56
+ label?: ValueResolver<T>
57
+ value?: ValueResolver<T>
58
+ transform?: ((options: FilterOption[]) => FilterOption[]) | null
59
+ toQuery?: ((value: unknown) => Record<string, unknown>) | null
60
+ }
61
+
62
+ /**
63
+ * Minimal interface for EntityManager (to avoid circular deps)
64
+ */
65
+ interface EntityManagerLike {
66
+ _cache?: unknown[]
67
+ list(params?: { page_size?: number }): Promise<{ items: unknown[] }>
68
+ }
69
+
70
+ /**
71
+ * Minimal interface for Orchestrator (to avoid circular deps)
72
+ */
73
+ interface OrchestratorLike {
74
+ get(name: string): EntityManagerLike | undefined
75
+ }
76
+
77
+ /**
78
+ * Minimal interface for SignalBus (to avoid circular deps)
79
+ */
80
+ interface SignalBusLike {
81
+ on(signal: string, handler: () => void): () => void
82
+ }
83
+
84
+ /**
85
+ * Resolve a value from an item using a path or function
86
+ */
87
+ function resolveValue<T>(item: T, resolver: ValueResolver<T>): unknown {
88
+ if (typeof resolver === 'function') {
89
+ return resolver(item)
90
+ }
91
+
92
+ // Use shared getNestedValue for path resolution
93
+ return getNestedValue(resolver, item)
94
+ }
95
+
96
+ export class FilterQuery<T = unknown> {
97
+ source: FilterQuerySource
98
+ entity: string | null
99
+ field: string | null
100
+ parentManager: EntityManagerLike | null
101
+ label: ValueResolver<T>
102
+ value: ValueResolver<T>
103
+ transform: ((options: FilterOption[]) => FilterOption[]) | null
104
+ toQuery: ((value: unknown) => Record<string, unknown>) | null
105
+
106
+ private _options: FilterOption[] | null = null
107
+ private _signals: SignalBusLike | null = null
108
+ private _subscriptions: Array<() => void> = []
109
+
110
+ constructor(options: FilterQueryOptions<T>) {
111
+ const {
112
+ source,
113
+ entity = null,
114
+ field = null,
115
+ parentManager = null,
116
+ label = 'name',
117
+ value = 'id',
118
+ transform = null,
119
+ toQuery = null,
120
+ } = options
121
+
122
+ // Validate source type
123
+ if (!source || !VALID_SOURCES.includes(source)) {
124
+ throw new Error(`FilterQuery: source must be one of: ${VALID_SOURCES.join(', ')}. Got: ${source}`)
125
+ }
126
+
127
+ // Validate source-specific requirements
128
+ if (source === 'entity' && !entity) {
129
+ throw new Error('FilterQuery: entity is required when source is "entity"')
130
+ }
131
+
132
+ if (source === 'field' && !field) {
133
+ throw new Error('FilterQuery: field is required when source is "field"')
134
+ }
135
+
136
+ this.source = source
137
+ this.entity = entity
138
+ this.field = field
139
+ this.parentManager = parentManager
140
+ this.label = label
141
+ this.value = value
142
+ this.transform = transform
143
+ this.toQuery = toQuery
144
+ }
145
+
146
+ /**
147
+ * Set the parent manager (called by useListPage for field source)
148
+ */
149
+ setParentManager(manager: EntityManagerLike): this {
150
+ this.parentManager = manager
151
+ return this
152
+ }
153
+
154
+ /**
155
+ * Set the SignalBus for cache invalidation
156
+ *
157
+ * Subscribes to entity CRUD signals ({entity}:created, {entity}:updated, {entity}:deleted)
158
+ * to invalidate cached options when the source entity changes.
159
+ */
160
+ setSignals(signals: SignalBusLike | null): this {
161
+ // Clean up existing subscriptions if any
162
+ this._cleanupSubscriptions()
163
+
164
+ this._signals = signals
165
+
166
+ if (!signals) return this
167
+
168
+ // Subscribe to entity CRUD signals for cache invalidation
169
+ if (this.source === 'entity' && this.entity) {
170
+ const actions = ['created', 'updated', 'deleted']
171
+ for (const action of actions) {
172
+ const signalName = `${this.entity}:${action}`
173
+ const unbind = signals.on(signalName, () => this.invalidate())
174
+ this._subscriptions.push(unbind)
175
+ }
176
+ }
177
+
178
+ return this
179
+ }
180
+
181
+ /**
182
+ * Cleanup signal subscriptions
183
+ */
184
+ private _cleanupSubscriptions(): void {
185
+ for (const unbind of this._subscriptions) {
186
+ if (typeof unbind === 'function') {
187
+ unbind()
188
+ }
189
+ }
190
+ this._subscriptions = []
191
+ }
192
+
193
+ /**
194
+ * Dispose the FilterQuery, cleaning up signal subscriptions
195
+ *
196
+ * Call this when the FilterQuery is no longer needed to prevent memory leaks.
197
+ */
198
+ dispose(): void {
199
+ this._cleanupSubscriptions()
200
+ this._signals = null
201
+ this._options = null
202
+ }
203
+
204
+ /**
205
+ * Invalidate the cached options (forces refresh on next getOptions)
206
+ */
207
+ invalidate(): void {
208
+ this._options = null
209
+ }
210
+
211
+ /**
212
+ * Get options for dropdown
213
+ */
214
+ async getOptions(orchestrator: OrchestratorLike | null = null): Promise<FilterOption[]> {
215
+ // Return cached options if available
216
+ if (this._options !== null) {
217
+ return this._options
218
+ }
219
+
220
+ let items: unknown[] = []
221
+
222
+ if (this.source === 'entity') {
223
+ items = await this._fetchFromEntity(orchestrator)
224
+ } else if (this.source === 'field') {
225
+ items = this._extractFromField()
226
+ }
227
+
228
+ // Map items to { label, value } format
229
+ let options: FilterOption[] = items.map((item) => {
230
+ const labelValue = resolveValue(item as T, this.label)
231
+ return {
232
+ label: labelValue == null ? labelValue : String(labelValue),
233
+ value: resolveValue(item as T, this.value),
234
+ }
235
+ })
236
+
237
+ // Apply transform if provided
238
+ if (typeof this.transform === 'function') {
239
+ options = this.transform(options)
240
+ }
241
+
242
+ // Cache the result
243
+ this._options = options
244
+
245
+ return options
246
+ }
247
+
248
+ /**
249
+ * Fetch items from entity manager
250
+ */
251
+ private async _fetchFromEntity(orchestrator: OrchestratorLike | null): Promise<unknown[]> {
252
+ if (!orchestrator) {
253
+ throw new Error('FilterQuery: orchestrator is required for entity source')
254
+ }
255
+
256
+ const manager = orchestrator.get(this.entity!)
257
+ if (!manager) {
258
+ throw new Error(`FilterQuery: entity "${this.entity}" not found in orchestrator`)
259
+ }
260
+
261
+ // Fetch all items (large page_size to get everything)
262
+ const result = await manager.list({ page_size: 1000 })
263
+ return result.items || []
264
+ }
265
+
266
+ /**
267
+ * Extract unique values from parent manager's cached items
268
+ */
269
+ private _extractFromField(): unknown[] {
270
+ if (!this.parentManager) {
271
+ throw new Error('FilterQuery: parentManager is required for field source')
272
+ }
273
+
274
+ // Get cached items from parent manager
275
+ const cachedItems = this.parentManager._cache || []
276
+
277
+ if (cachedItems.length === 0) {
278
+ return []
279
+ }
280
+
281
+ // Extract unique values from the field
282
+ const seen = new Set<string>()
283
+ const items: unknown[] = []
284
+
285
+ for (const item of cachedItems) {
286
+ const fieldValue = resolveValue(item as T, this.field!)
287
+ if (fieldValue == null) continue
288
+
289
+ // Create a unique key for deduplication
290
+ const key = typeof fieldValue === 'object' ? JSON.stringify(fieldValue) : String(fieldValue)
291
+
292
+ if (!seen.has(key)) {
293
+ seen.add(key)
294
+ // For field source, the item IS the value (create a simple object)
295
+ items.push({
296
+ [this.field!]: fieldValue,
297
+ // Also set 'name' and 'id' defaults for simple resolution
298
+ name: fieldValue,
299
+ id: fieldValue,
300
+ })
301
+ }
302
+ }
303
+
304
+ return items
305
+ }
306
+ }