@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- 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 +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- 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 +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- 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
|
+
}
|