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