@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ActionButtons - Renders action buttons from useTableActions
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - actions: Array of action objects from getActions(row)
|
|
7
|
+
*
|
|
8
|
+
* Each action object should have:
|
|
9
|
+
* - icon: PrimeVue icon class
|
|
10
|
+
* - tooltip: Tooltip text
|
|
11
|
+
* - severity: Button severity
|
|
12
|
+
* - handler: Click handler function
|
|
13
|
+
* - isDisabled: Boolean disabled state
|
|
14
|
+
*/
|
|
15
|
+
import Button from 'primevue/button'
|
|
16
|
+
|
|
17
|
+
interface Action {
|
|
18
|
+
name: string
|
|
19
|
+
icon?: string
|
|
20
|
+
tooltip?: string
|
|
21
|
+
severity: string
|
|
22
|
+
handler: () => void | Promise<void>
|
|
23
|
+
isDisabled?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
actions: Action[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
defineProps<Props>()
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="table-actions">
|
|
35
|
+
<Button
|
|
36
|
+
v-for="action in actions"
|
|
37
|
+
:key="action.name"
|
|
38
|
+
:icon="action.icon"
|
|
39
|
+
:severity="action.severity"
|
|
40
|
+
text
|
|
41
|
+
rounded
|
|
42
|
+
:disabled="action.isDisabled"
|
|
43
|
+
@click="action.handler"
|
|
44
|
+
v-tooltip.top="action.tooltip"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<!-- Styles in main.css: .table-actions -->
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ActionColumn - Reusable table action buttons (edit, delete)
|
|
4
|
+
*
|
|
5
|
+
* Slots:
|
|
6
|
+
* - default: Extra actions (after edit/delete)
|
|
7
|
+
*
|
|
8
|
+
* Emits:
|
|
9
|
+
* - edit: When edit button is clicked
|
|
10
|
+
* - delete: When delete button is clicked
|
|
11
|
+
*/
|
|
12
|
+
import Button from 'primevue/button'
|
|
13
|
+
|
|
14
|
+
defineEmits<{
|
|
15
|
+
edit: []
|
|
16
|
+
delete: []
|
|
17
|
+
}>()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div class="table-actions">
|
|
22
|
+
<Button
|
|
23
|
+
icon="pi pi-pencil"
|
|
24
|
+
severity="secondary"
|
|
25
|
+
text
|
|
26
|
+
rounded
|
|
27
|
+
@click="$emit('edit')"
|
|
28
|
+
v-tooltip.top="'Edit'"
|
|
29
|
+
/>
|
|
30
|
+
<Button
|
|
31
|
+
icon="pi pi-trash"
|
|
32
|
+
severity="danger"
|
|
33
|
+
text
|
|
34
|
+
rounded
|
|
35
|
+
@click="$emit('delete')"
|
|
36
|
+
v-tooltip.top="'Delete'"
|
|
37
|
+
/>
|
|
38
|
+
<slot ></slot>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FilterBar - Reusable filter bar with search field
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - search: Current search value (v-model compatible)
|
|
7
|
+
* - placeholder: Search input placeholder
|
|
8
|
+
*
|
|
9
|
+
* Slots:
|
|
10
|
+
* - default: Custom filters (left side, before search)
|
|
11
|
+
*
|
|
12
|
+
* Emits:
|
|
13
|
+
* - update:search: When search value changes
|
|
14
|
+
*/
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import InputText from 'primevue/inputtext'
|
|
17
|
+
import InputIcon from 'primevue/inputicon'
|
|
18
|
+
import IconField from 'primevue/iconfield'
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
modelValue?: string
|
|
22
|
+
placeholder?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
26
|
+
modelValue: '',
|
|
27
|
+
placeholder: 'Search...'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
'update:modelValue': [value: string]
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const searchModel = computed({
|
|
35
|
+
get: () => props.modelValue,
|
|
36
|
+
set: (value: string) => emit('update:modelValue', value)
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div class="filter-bar">
|
|
42
|
+
<div class="filter-bar-left">
|
|
43
|
+
<IconField>
|
|
44
|
+
<InputIcon class="pi pi-search" />
|
|
45
|
+
<InputText
|
|
46
|
+
v-model="searchModel"
|
|
47
|
+
:placeholder="placeholder"
|
|
48
|
+
:class="{ 'filter-active': searchModel }"
|
|
49
|
+
/>
|
|
50
|
+
</IconField>
|
|
51
|
+
<slot ></slot>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ListPage - Unified list page component
|
|
4
|
+
*
|
|
5
|
+
* Renders a complete CRUD list page with:
|
|
6
|
+
* - PageHeader with title and create button
|
|
7
|
+
* - CardsGrid for stats/custom cards
|
|
8
|
+
* - FilterBar with search and custom filters
|
|
9
|
+
* - DataTable with actions column
|
|
10
|
+
*
|
|
11
|
+
* Props come from useListPage composable
|
|
12
|
+
*
|
|
13
|
+
* Filter types:
|
|
14
|
+
* - 'select' (default): Standard dropdown
|
|
15
|
+
* - 'autocomplete': Searchable dropdown with type-ahead
|
|
16
|
+
*/
|
|
17
|
+
import { computed, ref, watch, onMounted, onUnmounted, type PropType } from 'vue'
|
|
18
|
+
import PageHeader from '../layout/PageHeader.vue'
|
|
19
|
+
import CardsGrid from '../display/CardsGrid.vue'
|
|
20
|
+
import FilterBar from './FilterBar.vue'
|
|
21
|
+
import ActionButtons from './ActionButtons.vue'
|
|
22
|
+
import DataTable from 'primevue/datatable'
|
|
23
|
+
import Column from 'primevue/column'
|
|
24
|
+
import Button from 'primevue/button'
|
|
25
|
+
import Select from 'primevue/select'
|
|
26
|
+
import AutoComplete from 'primevue/autocomplete'
|
|
27
|
+
import SplitButton from 'primevue/splitbutton'
|
|
28
|
+
import type { FilterConfig, CardConfig, ResolvedAction, ResolvedHeaderAction as BaseResolvedHeaderAction } from '../../composables/useListPage'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Header action state for label function (matches useListPage)
|
|
32
|
+
*/
|
|
33
|
+
interface HeaderActionState {
|
|
34
|
+
hasSelection: boolean
|
|
35
|
+
selectionCount: number
|
|
36
|
+
deleting: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Label type from HeaderActionConfig
|
|
41
|
+
*/
|
|
42
|
+
type LabelType = string | ((state: HeaderActionState) => string)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extended header action with optional size property used in template
|
|
46
|
+
*/
|
|
47
|
+
interface ResolvedHeaderAction extends BaseResolvedHeaderAction {
|
|
48
|
+
size?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Autocomplete event interface
|
|
53
|
+
*/
|
|
54
|
+
interface AutocompleteEvent {
|
|
55
|
+
query?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Autocomplete select event interface
|
|
60
|
+
*/
|
|
61
|
+
interface AutocompleteSelectEvent {
|
|
62
|
+
value: FilterOption | string | null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Filter option interface
|
|
67
|
+
*/
|
|
68
|
+
interface FilterOption {
|
|
69
|
+
label: string
|
|
70
|
+
value: unknown
|
|
71
|
+
[key: string]: unknown
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* SplitButton menu item
|
|
76
|
+
*/
|
|
77
|
+
interface SplitButtonMenuItem {
|
|
78
|
+
label: string
|
|
79
|
+
icon?: string
|
|
80
|
+
disabled?: boolean
|
|
81
|
+
command?: () => void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
// Header
|
|
86
|
+
title: { type: String, default: '' },
|
|
87
|
+
subtitle: { type: String as PropType<string | null>, default: null },
|
|
88
|
+
headerActions: { type: Array as PropType<ResolvedHeaderAction[]>, default: () => [] },
|
|
89
|
+
|
|
90
|
+
// Cards
|
|
91
|
+
cards: { type: Array as PropType<CardConfig[]>, default: () => [] },
|
|
92
|
+
cardsColumns: { type: [Number, String] as PropType<number | 'auto'>, default: 'auto' },
|
|
93
|
+
|
|
94
|
+
// Table data
|
|
95
|
+
items: { type: Array as PropType<unknown[]>, required: true },
|
|
96
|
+
loading: { type: Boolean, default: false },
|
|
97
|
+
dataKey: { type: String, default: 'id' },
|
|
98
|
+
|
|
99
|
+
// Selection
|
|
100
|
+
selected: { type: Array as PropType<unknown[]>, default: () => [] },
|
|
101
|
+
selectable: { type: Boolean, default: false },
|
|
102
|
+
|
|
103
|
+
// Pagination
|
|
104
|
+
paginator: { type: Boolean, default: true },
|
|
105
|
+
rows: { type: Number, default: 10 },
|
|
106
|
+
rowsPerPageOptions: { type: Array as PropType<number[]>, default: () => [10, 50, 100] },
|
|
107
|
+
totalRecords: { type: Number, default: 0 },
|
|
108
|
+
lazy: { type: Boolean, default: false },
|
|
109
|
+
|
|
110
|
+
// Sorting
|
|
111
|
+
sortField: { type: String as PropType<string | null>, default: null },
|
|
112
|
+
sortOrder: { type: Number, default: 1 },
|
|
113
|
+
|
|
114
|
+
// Search
|
|
115
|
+
searchQuery: { type: String, default: '' },
|
|
116
|
+
searchPlaceholder: { type: String, default: 'Search...' },
|
|
117
|
+
|
|
118
|
+
// Filters
|
|
119
|
+
filters: { type: Array as PropType<FilterConfig[]>, default: () => [] },
|
|
120
|
+
filterValues: { type: Object as PropType<Record<string, unknown>>, default: () => ({}) },
|
|
121
|
+
isFilterAtDefault: { type: Function as PropType<(name: string) => boolean>, default: () => () => true },
|
|
122
|
+
|
|
123
|
+
// Row Actions
|
|
124
|
+
getActions: { type: Function as unknown as PropType<((row: unknown) => ResolvedAction[]) | null>, default: null },
|
|
125
|
+
actionsWidth: { type: String, default: '120px' },
|
|
126
|
+
|
|
127
|
+
// Mobile row tap
|
|
128
|
+
hasRowTapAction: { type: Boolean, default: false }
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
function resolveLabel(label: LabelType): string {
|
|
132
|
+
if (typeof label === 'function') {
|
|
133
|
+
const state: HeaderActionState = {
|
|
134
|
+
hasSelection: (props.selected?.length || 0) > 0,
|
|
135
|
+
selectionCount: props.selected?.length || 0,
|
|
136
|
+
deleting: false
|
|
137
|
+
}
|
|
138
|
+
return label(state)
|
|
139
|
+
}
|
|
140
|
+
return label
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Local copy of filterValues for proper v-model binding
|
|
144
|
+
// PrimeVue Select needs v-model to fire change events reliably
|
|
145
|
+
const localFilterValues = ref<Record<string, unknown>>({})
|
|
146
|
+
|
|
147
|
+
// Autocomplete suggestions per filter (keyed by filter name)
|
|
148
|
+
const autocompleteSuggestions = ref<Record<string, FilterOption[]>>({})
|
|
149
|
+
|
|
150
|
+
// For autocomplete filters, we store the selected option object (not just value)
|
|
151
|
+
// to display the label properly. This maps filter.name -> selected option object
|
|
152
|
+
const autocompleteSelected = ref<Record<string, FilterOption | null>>({})
|
|
153
|
+
|
|
154
|
+
// Sync from prop to local (when parent updates)
|
|
155
|
+
watch(() => props.filterValues, (newVal: Record<string, unknown>) => {
|
|
156
|
+
localFilterValues.value = { ...newVal }
|
|
157
|
+
// Sync autocomplete selections - find matching option by value
|
|
158
|
+
for (const filter of props.filters) {
|
|
159
|
+
if (filter.type === 'autocomplete' && newVal[filter.name] != null) {
|
|
160
|
+
const option = filter.options?.find((opt: FilterOption) =>
|
|
161
|
+
(opt.value ?? opt) === newVal[filter.name]
|
|
162
|
+
)
|
|
163
|
+
if (option) {
|
|
164
|
+
autocompleteSelected.value[filter.name] = option as FilterOption
|
|
165
|
+
}
|
|
166
|
+
} else if (filter.type === 'autocomplete' && newVal[filter.name] == null) {
|
|
167
|
+
autocompleteSelected.value[filter.name] = null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, { immediate: true, deep: true })
|
|
171
|
+
|
|
172
|
+
// Check if any filter or search has a non-null value
|
|
173
|
+
const hasActiveFilters = computed(() => {
|
|
174
|
+
const hasFilters = Object.values(props.filterValues).some((v: unknown) => v !== null && v !== undefined && v !== '')
|
|
175
|
+
const hasSearch = props.searchQuery && props.searchQuery.trim() !== ''
|
|
176
|
+
return hasFilters || hasSearch
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const emit = defineEmits<{
|
|
180
|
+
(e: 'update:selected', value: unknown[]): void
|
|
181
|
+
(e: 'update:searchQuery', value: string): void
|
|
182
|
+
(e: 'update:filterValues', values: Record<string, unknown>): void
|
|
183
|
+
(e: 'page', event: { page: number; rows: number }): void
|
|
184
|
+
(e: 'sort', event: { sortField: string; sortOrder: number }): void
|
|
185
|
+
(e: 'row-click', data: unknown, originalEvent: MouseEvent): void
|
|
186
|
+
}>()
|
|
187
|
+
|
|
188
|
+
// Mobile detection for header actions dropdown
|
|
189
|
+
const isMobile = ref<boolean>(window.innerWidth < 768)
|
|
190
|
+
function handleResize(): void {
|
|
191
|
+
isMobile.value = window.innerWidth < 768
|
|
192
|
+
}
|
|
193
|
+
onMounted(() => window.addEventListener('resize', handleResize))
|
|
194
|
+
onUnmounted(() => window.removeEventListener('resize', handleResize))
|
|
195
|
+
|
|
196
|
+
// Header actions for mobile split button
|
|
197
|
+
// Primary action = last (rightmost, e.g. Delete selected), dropdown = rest (e.g. Create)
|
|
198
|
+
const primaryHeaderAction = computed(() => {
|
|
199
|
+
if (props.headerActions.length === 0) return null
|
|
200
|
+
return props.headerActions[props.headerActions.length - 1] as ResolvedHeaderAction
|
|
201
|
+
})
|
|
202
|
+
const dropdownHeaderActions = computed(() => {
|
|
203
|
+
if (props.headerActions.length <= 1) return [] as SplitButtonMenuItem[]
|
|
204
|
+
return props.headerActions.slice(0, -1).map((action) => ({
|
|
205
|
+
label: resolveLabel(action.label),
|
|
206
|
+
icon: action.icon,
|
|
207
|
+
disabled: action.isLoading,
|
|
208
|
+
command: action.onClick
|
|
209
|
+
})) as SplitButtonMenuItem[]
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
function clearAllFilters(): void {
|
|
213
|
+
// Build empty filter values object
|
|
214
|
+
const cleared: Record<string, null> = {}
|
|
215
|
+
for (const key of Object.keys(localFilterValues.value)) {
|
|
216
|
+
cleared[key] = null
|
|
217
|
+
}
|
|
218
|
+
// Update local ref first (for immediate UI feedback)
|
|
219
|
+
localFilterValues.value = cleared
|
|
220
|
+
// Clear autocomplete selections too
|
|
221
|
+
autocompleteSelected.value = {}
|
|
222
|
+
// Then emit to parent
|
|
223
|
+
emit('update:filterValues', cleared)
|
|
224
|
+
// Also clear search
|
|
225
|
+
emit('update:searchQuery', '')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onSelectionChange(value: unknown[]): void {
|
|
229
|
+
emit('update:selected', value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function onSearchChange(value: string): void {
|
|
233
|
+
emit('update:searchQuery', value)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function onFilterChange(_name: string): void {
|
|
237
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Handle autocomplete search/filter
|
|
242
|
+
* @param event - PrimeVue autocomplete event with query property
|
|
243
|
+
* @param filter - Filter definition from props.filters
|
|
244
|
+
*/
|
|
245
|
+
function onAutocompleteSearch(event: AutocompleteEvent, filter: FilterConfig): void {
|
|
246
|
+
const query = (event.query || '').toLowerCase()
|
|
247
|
+
const labelField = filter.optionLabel || 'label'
|
|
248
|
+
|
|
249
|
+
if (!query) {
|
|
250
|
+
// Show all options when query is empty
|
|
251
|
+
autocompleteSuggestions.value[filter.name] = [...(filter.options || [])] as FilterOption[]
|
|
252
|
+
} else {
|
|
253
|
+
// Filter options by label
|
|
254
|
+
autocompleteSuggestions.value[filter.name] = (filter.options || []).filter((opt: FilterOption) => {
|
|
255
|
+
const label = typeof opt === 'string' ? opt : ((opt as FilterOption)[labelField] || '')
|
|
256
|
+
return String(label).toLowerCase().includes(query)
|
|
257
|
+
}) as FilterOption[]
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Handle autocomplete selection
|
|
263
|
+
* @param event - PrimeVue event with value property (selected option)
|
|
264
|
+
* @param filter - Filter definition
|
|
265
|
+
*/
|
|
266
|
+
function onAutocompleteSelect(event: AutocompleteSelectEvent, filter: FilterConfig): void {
|
|
267
|
+
const selected = event.value
|
|
268
|
+
const valueField = filter.optionValue || 'value'
|
|
269
|
+
|
|
270
|
+
// Store the selected option for display
|
|
271
|
+
autocompleteSelected.value[filter.name] = selected as FilterOption | null
|
|
272
|
+
|
|
273
|
+
// Extract the actual value to send to API
|
|
274
|
+
const value = selected != null
|
|
275
|
+
? (typeof selected === 'string' ? selected : (selected as FilterOption)[valueField])
|
|
276
|
+
: null
|
|
277
|
+
|
|
278
|
+
localFilterValues.value[filter.name] = value
|
|
279
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Handle autocomplete clear (user clears the input)
|
|
284
|
+
*/
|
|
285
|
+
function onAutocompleteClear(filter: FilterConfig): void {
|
|
286
|
+
autocompleteSelected.value[filter.name] = null
|
|
287
|
+
localFilterValues.value[filter.name] = null
|
|
288
|
+
emit('update:filterValues', { ...localFilterValues.value })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function onPage(event: { page: number; rows: number }): void {
|
|
292
|
+
emit('page', event)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function onSort(event: unknown): void {
|
|
296
|
+
const e = event as { sortField?: string | null; sortOrder?: number | null }
|
|
297
|
+
emit('sort', { sortField: String(e.sortField ?? ''), sortOrder: e.sortOrder ?? 1 })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function onRowClick(event: { data: unknown; originalEvent: Event }): void {
|
|
301
|
+
// event.data = row data, event.originalEvent = Event
|
|
302
|
+
emit('row-click', event.data, event.originalEvent as MouseEvent)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get filter CSS class based on value and default state
|
|
307
|
+
* - No value (null/empty): no class
|
|
308
|
+
* - Value = default: 'filter-default' (blue/info)
|
|
309
|
+
* - Value ≠ default: 'filter-modified' (orange/warning)
|
|
310
|
+
*/
|
|
311
|
+
function getFilterClass(filter: FilterConfig): Record<string, boolean> {
|
|
312
|
+
const value = localFilterValues.value[filter.name]
|
|
313
|
+
const hasValue = value != null && value !== ''
|
|
314
|
+
if (!hasValue) return {}
|
|
315
|
+
|
|
316
|
+
const isDefault = props.isFilterAtDefault(filter.name)
|
|
317
|
+
return {
|
|
318
|
+
'filter-default': isDefault,
|
|
319
|
+
'filter-modified': !isDefault
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<template>
|
|
325
|
+
<div>
|
|
326
|
+
<!-- Nav slot for PageNav (child routes) -->
|
|
327
|
+
<slot name="nav" />
|
|
328
|
+
|
|
329
|
+
<PageHeader :title="title" :subtitle="subtitle">
|
|
330
|
+
<template #actions>
|
|
331
|
+
<slot name="header-actions" ></slot>
|
|
332
|
+
<!-- Mobile: split button (primary action + dropdown) -->
|
|
333
|
+
<template v-if="isMobile && headerActions.length > 0 && primaryHeaderAction">
|
|
334
|
+
<!-- Single action: just a button -->
|
|
335
|
+
<Button
|
|
336
|
+
v-if="headerActions.length === 1"
|
|
337
|
+
:label="resolveLabel(primaryHeaderAction.label)"
|
|
338
|
+
:icon="primaryHeaderAction.icon"
|
|
339
|
+
:severity="primaryHeaderAction.severity"
|
|
340
|
+
:size="primaryHeaderAction.size || 'small'"
|
|
341
|
+
:loading="primaryHeaderAction.isLoading"
|
|
342
|
+
@click="primaryHeaderAction.onClick"
|
|
343
|
+
/>
|
|
344
|
+
<!-- Multiple actions: split button -->
|
|
345
|
+
<SplitButton
|
|
346
|
+
v-else
|
|
347
|
+
:label="resolveLabel(primaryHeaderAction.label)"
|
|
348
|
+
:icon="primaryHeaderAction.icon"
|
|
349
|
+
:severity="primaryHeaderAction.severity"
|
|
350
|
+
:model="dropdownHeaderActions"
|
|
351
|
+
size="small"
|
|
352
|
+
@click="primaryHeaderAction.onClick"
|
|
353
|
+
/>
|
|
354
|
+
</template>
|
|
355
|
+
<!-- Desktop: individual buttons -->
|
|
356
|
+
<template v-else>
|
|
357
|
+
<Button
|
|
358
|
+
v-for="action in headerActions"
|
|
359
|
+
:key="action.name"
|
|
360
|
+
:label="resolveLabel(action.label)"
|
|
361
|
+
:icon="action.icon"
|
|
362
|
+
:severity="action.severity"
|
|
363
|
+
:size="action.size || 'small'"
|
|
364
|
+
:loading="action.isLoading"
|
|
365
|
+
@click="action.onClick"
|
|
366
|
+
/>
|
|
367
|
+
</template>
|
|
368
|
+
</template>
|
|
369
|
+
</PageHeader>
|
|
370
|
+
|
|
371
|
+
<!-- Cards Zone -->
|
|
372
|
+
<CardsGrid :cards="cards" :columns="cardsColumns">
|
|
373
|
+
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
|
|
374
|
+
<slot :name="slotName" v-bind="slotProps" ></slot>
|
|
375
|
+
</template>
|
|
376
|
+
</CardsGrid>
|
|
377
|
+
|
|
378
|
+
<!-- Before Table Slot -->
|
|
379
|
+
<slot name="beforeTable" ></slot>
|
|
380
|
+
|
|
381
|
+
<div class="card">
|
|
382
|
+
<!-- Filter Bar -->
|
|
383
|
+
<FilterBar
|
|
384
|
+
:modelValue="searchQuery"
|
|
385
|
+
@update:modelValue="onSearchChange"
|
|
386
|
+
:placeholder="searchPlaceholder"
|
|
387
|
+
>
|
|
388
|
+
<template v-for="filter in filters" :key="filter.name">
|
|
389
|
+
<!-- Autocomplete filter -->
|
|
390
|
+
<AutoComplete
|
|
391
|
+
v-if="filter.type === 'autocomplete'"
|
|
392
|
+
v-model="autocompleteSelected[filter.name]"
|
|
393
|
+
:suggestions="autocompleteSuggestions[filter.name] || []"
|
|
394
|
+
@complete="onAutocompleteSearch($event, filter)"
|
|
395
|
+
@item-select="onAutocompleteSelect($event, filter)"
|
|
396
|
+
@clear="onAutocompleteClear(filter)"
|
|
397
|
+
:optionLabel="filter.optionLabel || 'label'"
|
|
398
|
+
:placeholder="filter.placeholder"
|
|
399
|
+
:dropdown="true"
|
|
400
|
+
:minLength="0"
|
|
401
|
+
:style="{ minWidth: filter.width || '160px' }"
|
|
402
|
+
:class="getFilterClass(filter)"
|
|
403
|
+
:inputClass="'filter-autocomplete-input'"
|
|
404
|
+
/>
|
|
405
|
+
<!-- Standard select filter -->
|
|
406
|
+
<Select
|
|
407
|
+
v-else
|
|
408
|
+
v-model="localFilterValues[filter.name]"
|
|
409
|
+
@update:modelValue="onFilterChange(filter.name)"
|
|
410
|
+
:options="filter.options"
|
|
411
|
+
:optionLabel="filter.optionLabel || 'label'"
|
|
412
|
+
:optionValue="filter.optionValue || 'value'"
|
|
413
|
+
:placeholder="filter.placeholder"
|
|
414
|
+
:style="{ minWidth: filter.width || '160px' }"
|
|
415
|
+
:class="getFilterClass(filter)"
|
|
416
|
+
/>
|
|
417
|
+
</template>
|
|
418
|
+
<slot name="filters" ></slot>
|
|
419
|
+
<Button
|
|
420
|
+
v-if="hasActiveFilters"
|
|
421
|
+
icon="pi pi-filter-slash"
|
|
422
|
+
severity="secondary"
|
|
423
|
+
text
|
|
424
|
+
rounded
|
|
425
|
+
size="small"
|
|
426
|
+
@click="clearAllFilters"
|
|
427
|
+
v-tooltip.top="'Clear filters'"
|
|
428
|
+
/>
|
|
429
|
+
</FilterBar>
|
|
430
|
+
|
|
431
|
+
<!-- Data Table -->
|
|
432
|
+
<DataTable
|
|
433
|
+
:value="items"
|
|
434
|
+
:loading="loading"
|
|
435
|
+
:dataKey="dataKey"
|
|
436
|
+
:paginator="paginator"
|
|
437
|
+
:rows="rows"
|
|
438
|
+
:rowsPerPageOptions="rowsPerPageOptions"
|
|
439
|
+
:totalRecords="totalRecords"
|
|
440
|
+
:lazy="lazy"
|
|
441
|
+
:sortField="sortField ?? undefined"
|
|
442
|
+
:sortOrder="sortOrder"
|
|
443
|
+
:selection="selected"
|
|
444
|
+
@update:selection="onSelectionChange"
|
|
445
|
+
@page="onPage"
|
|
446
|
+
@sort="onSort"
|
|
447
|
+
@row-click="onRowClick"
|
|
448
|
+
:class="{ 'datatable-row-clickable': hasRowTapAction }"
|
|
449
|
+
stripedRows
|
|
450
|
+
removableSort
|
|
451
|
+
>
|
|
452
|
+
<!-- Columns from slot -->
|
|
453
|
+
<slot name="columns" ></slot>
|
|
454
|
+
|
|
455
|
+
<!-- Actions column -->
|
|
456
|
+
<Column v-if="getActions" header="Actions" :style="{ width: actionsWidth }">
|
|
457
|
+
<template #body="{ data }">
|
|
458
|
+
<ActionButtons :actions="getActions(data)" />
|
|
459
|
+
</template>
|
|
460
|
+
</Column>
|
|
461
|
+
|
|
462
|
+
<!-- Selection column -->
|
|
463
|
+
<Column v-if="selectable" selectionMode="multiple" headerStyle="width: 3rem" />
|
|
464
|
+
</DataTable>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</template>
|