qdadm 0.15.1 → 0.17.0
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/README.md +153 -1
- package/package.json +15 -2
- package/src/components/BoolCell.vue +11 -6
- package/src/components/forms/FormField.vue +64 -6
- package/src/components/forms/FormPage.vue +276 -0
- package/src/components/index.js +11 -0
- package/src/components/layout/AppLayout.vue +18 -9
- package/src/components/layout/BaseLayout.vue +183 -0
- package/src/components/layout/DashboardLayout.vue +100 -0
- package/src/components/layout/FormLayout.vue +261 -0
- package/src/components/layout/ListLayout.vue +334 -0
- package/src/components/layout/PageHeader.vue +6 -9
- package/src/components/layout/PageNav.vue +15 -0
- package/src/components/layout/Zone.vue +165 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +140 -0
- package/src/components/layout/defaults/DefaultFooter.vue +56 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +53 -0
- package/src/components/layout/defaults/DefaultHeader.vue +69 -0
- package/src/components/layout/defaults/DefaultMenu.vue +197 -0
- package/src/components/layout/defaults/DefaultPagination.vue +79 -0
- package/src/components/layout/defaults/DefaultTable.vue +130 -0
- package/src/components/layout/defaults/DefaultToaster.vue +16 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +96 -0
- package/src/components/layout/defaults/index.js +17 -0
- package/src/composables/index.js +8 -6
- package/src/composables/useBreadcrumb.js +9 -5
- package/src/composables/useForm.js +135 -0
- package/src/composables/useFormPageBuilder.js +1154 -0
- package/src/composables/useHooks.js +53 -0
- package/src/composables/useLayoutResolver.js +260 -0
- package/src/composables/useListPageBuilder.js +336 -52
- package/src/composables/useNavContext.js +372 -0
- package/src/composables/useNavigation.js +38 -2
- package/src/composables/usePageTitle.js +59 -0
- package/src/composables/useSignals.js +49 -0
- package/src/composables/useZoneRegistry.js +162 -0
- package/src/core/bundles.js +406 -0
- package/src/core/decorator.js +322 -0
- package/src/core/extension.js +386 -0
- package/src/core/index.js +28 -0
- package/src/entity/EntityManager.js +314 -16
- package/src/entity/auth/AuthAdapter.js +125 -0
- package/src/entity/auth/PermissiveAdapter.js +64 -0
- package/src/entity/auth/index.js +11 -0
- package/src/entity/index.js +3 -0
- package/src/entity/storage/MockApiStorage.js +349 -0
- package/src/entity/storage/SdkStorage.js +478 -0
- package/src/entity/storage/index.js +2 -0
- package/src/hooks/HookRegistry.js +411 -0
- package/src/hooks/index.js +12 -0
- package/src/index.js +12 -0
- package/src/kernel/Kernel.js +141 -4
- package/src/kernel/SignalBus.js +180 -0
- package/src/kernel/index.js +7 -0
- package/src/module/moduleRegistry.js +124 -6
- package/src/orchestrator/Orchestrator.js +73 -1
- package/src/plugin.js +5 -0
- package/src/zones/ZoneRegistry.js +821 -0
- package/src/zones/index.js +16 -0
- package/src/zones/zones.js +189 -0
- package/src/composables/useEntityTitle.js +0 -121
- package/src/composables/useManager.js +0 -20
- package/src/composables/usePageBuilder.js +0 -334
- package/src/composables/useStatus.js +0 -146
- package/src/composables/useSubEditor.js +0 -165
- package/src/composables/useTabSync.js +0 -110
|
@@ -2,6 +2,7 @@ import { ref, computed, watch, onMounted, inject, provide } from 'vue'
|
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
3
|
import { useToast } from 'primevue/usetoast'
|
|
4
4
|
import { useConfirm } from 'primevue/useconfirm'
|
|
5
|
+
import { useHooks } from './useHooks.js'
|
|
5
6
|
|
|
6
7
|
// Cookie utilities for pagination persistence
|
|
7
8
|
const COOKIE_NAME = 'qdadm_pageSize'
|
|
@@ -168,6 +169,9 @@ export function useListPageBuilder(config = {}) {
|
|
|
168
169
|
// Entity filters registry (optional, provided by consuming app)
|
|
169
170
|
const entityFilters = inject('qdadmEntityFilters', {})
|
|
170
171
|
|
|
172
|
+
// Get HookRegistry for list:alter hook (optional, may not exist in tests)
|
|
173
|
+
const hooks = useHooks()
|
|
174
|
+
|
|
171
175
|
// ============ STATE ============
|
|
172
176
|
const items = ref([])
|
|
173
177
|
const loading = ref(false)
|
|
@@ -192,6 +196,39 @@ export function useListPageBuilder(config = {}) {
|
|
|
192
196
|
debounce: 300
|
|
193
197
|
})
|
|
194
198
|
|
|
199
|
+
// ============ COLUMNS ============
|
|
200
|
+
const columnsMap = ref(new Map())
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Add a column to the list
|
|
204
|
+
* @param {string} field - Field name (unique identifier)
|
|
205
|
+
* @param {object} columnConfig - Column configuration
|
|
206
|
+
* @param {string} [columnConfig.header] - Column header label
|
|
207
|
+
* @param {boolean} [columnConfig.sortable] - Whether column is sortable
|
|
208
|
+
* @param {string} [columnConfig.style] - Inline style
|
|
209
|
+
* @param {Function} [columnConfig.body] - Custom body template function
|
|
210
|
+
*/
|
|
211
|
+
function addColumn(field, columnConfig = {}) {
|
|
212
|
+
columnsMap.value.set(field, {
|
|
213
|
+
field,
|
|
214
|
+
header: columnConfig.header || field.charAt(0).toUpperCase() + field.slice(1),
|
|
215
|
+
...columnConfig
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function removeColumn(field) {
|
|
220
|
+
columnsMap.value.delete(field)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateColumn(field, updates) {
|
|
224
|
+
const existing = columnsMap.value.get(field)
|
|
225
|
+
if (existing) {
|
|
226
|
+
columnsMap.value.set(field, { ...existing, ...updates })
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const columns = computed(() => Array.from(columnsMap.value.values()))
|
|
231
|
+
|
|
195
232
|
// ============ HEADER ACTIONS ============
|
|
196
233
|
const headerActionsMap = ref(new Map())
|
|
197
234
|
|
|
@@ -232,6 +269,48 @@ export function useListPageBuilder(config = {}) {
|
|
|
232
269
|
|
|
233
270
|
const headerActions = computed(() => getHeaderActions())
|
|
234
271
|
|
|
272
|
+
// ============ PERMISSION STATE ============
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether the current user can create new entities
|
|
276
|
+
* Reactive computed based on manager.canCreate()
|
|
277
|
+
*/
|
|
278
|
+
const canCreate = computed(() => manager.canCreate())
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Whether the current user can delete entities (general scope check)
|
|
282
|
+
* For row-level checks, use canDeleteRow(row)
|
|
283
|
+
*/
|
|
284
|
+
const canDelete = computed(() => manager.canDelete())
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check if user can edit a specific row (scope + silo check)
|
|
288
|
+
* @param {object} row - The row/record to check
|
|
289
|
+
* @returns {boolean}
|
|
290
|
+
*/
|
|
291
|
+
function canEditRow(row) {
|
|
292
|
+
return manager.canUpdate(row)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if user can delete a specific row (scope + silo check)
|
|
297
|
+
* @param {object} row - The row/record to check
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
function canDeleteRow(row) {
|
|
301
|
+
return manager.canDelete(row)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get actions for a row, filtering out those the user cannot perform
|
|
306
|
+
* This is the permission-aware version of getActions()
|
|
307
|
+
* @param {object} row - The row to get actions for
|
|
308
|
+
* @returns {Array} - Filtered list of actions the user can perform
|
|
309
|
+
*/
|
|
310
|
+
function getRowActions(row) {
|
|
311
|
+
return getActions(row)
|
|
312
|
+
}
|
|
313
|
+
|
|
235
314
|
/**
|
|
236
315
|
* Add standard "Create" header action
|
|
237
316
|
* Respects manager.canCreate() for visibility
|
|
@@ -520,70 +599,77 @@ export function useListPageBuilder(config = {}) {
|
|
|
520
599
|
|
|
521
600
|
/**
|
|
522
601
|
* Load filter options from API endpoints (for filters with optionsEndpoint)
|
|
602
|
+
* After loading, invokes filter:alter and {entity}:filter:alter hooks.
|
|
523
603
|
*/
|
|
524
604
|
async function loadFilterOptions() {
|
|
525
605
|
const entityConfig = entityFilters[entityName]
|
|
526
|
-
if (!entityConfig?.filters) return
|
|
527
606
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
let finalOptions = [{ label: 'All', value: null }]
|
|
607
|
+
// Load options from API endpoints if configured
|
|
608
|
+
if (entityConfig?.filters) {
|
|
609
|
+
for (const filterDef of entityConfig.filters) {
|
|
610
|
+
if (!filterDef.optionsEndpoint) continue
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
// Use manager.request for custom endpoints, or get another manager
|
|
614
|
+
const optionsManager = filterDef.optionsEntity
|
|
615
|
+
? orchestrator.get(filterDef.optionsEntity)
|
|
616
|
+
: manager
|
|
617
|
+
|
|
618
|
+
let rawOptions
|
|
619
|
+
if (filterDef.optionsEntity) {
|
|
620
|
+
const response = await optionsManager.list({ page_size: 1000 })
|
|
621
|
+
rawOptions = response.items || []
|
|
622
|
+
} else {
|
|
623
|
+
rawOptions = await manager.request('GET', filterDef.optionsEndpoint)
|
|
624
|
+
rawOptions = Array.isArray(rawOptions) ? rawOptions : rawOptions?.items || []
|
|
625
|
+
}
|
|
548
626
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
finalOptions.push(filterDef.includeNull)
|
|
552
|
-
}
|
|
627
|
+
// Build options array with "All" option first
|
|
628
|
+
let finalOptions = [{ label: 'All', value: null }]
|
|
553
629
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// labelFallback: function(value) for unknown values (default: snakeToTitle)
|
|
558
|
-
const labelField = filterDef.optionLabelField || 'label'
|
|
559
|
-
const valueField = filterDef.optionValueField || 'value'
|
|
560
|
-
const labelMap = filterDef.labelMap || {}
|
|
561
|
-
const labelFallback = filterDef.labelFallback || snakeToTitle
|
|
562
|
-
|
|
563
|
-
const mappedOptions = rawOptions.map(opt => {
|
|
564
|
-
const value = opt[valueField] ?? opt.id ?? opt
|
|
565
|
-
// Priority: labelMap > API label field > fallback function
|
|
566
|
-
let label = labelMap[value]
|
|
567
|
-
if (!label) {
|
|
568
|
-
label = opt[labelField] || opt.name
|
|
569
|
-
}
|
|
570
|
-
if (!label) {
|
|
571
|
-
label = labelFallback(value)
|
|
630
|
+
// Add null option if configured
|
|
631
|
+
if (filterDef.includeNull) {
|
|
632
|
+
finalOptions.push(filterDef.includeNull)
|
|
572
633
|
}
|
|
573
|
-
return { label, value }
|
|
574
|
-
})
|
|
575
634
|
|
|
576
|
-
|
|
635
|
+
// Map fetched options to standard { label, value } format
|
|
636
|
+
// optionLabelField/optionValueField specify which API fields to use
|
|
637
|
+
// labelMap: { value: 'Label' } for custom value-to-label mapping
|
|
638
|
+
// labelFallback: function(value) for unknown values (default: snakeToTitle)
|
|
639
|
+
const labelField = filterDef.optionLabelField || 'label'
|
|
640
|
+
const valueField = filterDef.optionValueField || 'value'
|
|
641
|
+
const labelMap = filterDef.labelMap || {}
|
|
642
|
+
const labelFallback = filterDef.labelFallback || snakeToTitle
|
|
643
|
+
|
|
644
|
+
const mappedOptions = rawOptions.map(opt => {
|
|
645
|
+
const value = opt[valueField] ?? opt.id ?? opt
|
|
646
|
+
// Priority: labelMap > API label field > fallback function
|
|
647
|
+
let label = labelMap[value]
|
|
648
|
+
if (!label) {
|
|
649
|
+
label = opt[labelField] || opt.name
|
|
650
|
+
}
|
|
651
|
+
if (!label) {
|
|
652
|
+
label = labelFallback(value)
|
|
653
|
+
}
|
|
654
|
+
return { label, value }
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
finalOptions = [...finalOptions, ...mappedOptions]
|
|
577
658
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
659
|
+
// Update filter options in map
|
|
660
|
+
const existing = filtersMap.value.get(filterDef.name)
|
|
661
|
+
if (existing) {
|
|
662
|
+
filtersMap.value.set(filterDef.name, { ...existing, options: finalOptions })
|
|
663
|
+
}
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.warn(`Failed to load options for filter ${filterDef.name}:`, error)
|
|
582
666
|
}
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.warn(`Failed to load options for filter ${filterDef.name}:`, error)
|
|
585
667
|
}
|
|
586
668
|
}
|
|
669
|
+
|
|
670
|
+
// Invoke filter:alter hooks after all options are loaded (always runs)
|
|
671
|
+
await invokeFilterAlterHook()
|
|
672
|
+
|
|
587
673
|
// Trigger Vue reactivity by replacing the Map reference
|
|
588
674
|
filtersMap.value = new Map(filtersMap.value)
|
|
589
675
|
}
|
|
@@ -915,6 +1001,184 @@ export function useListPageBuilder(config = {}) {
|
|
|
915
1001
|
// Note: filterValues changes are handled directly in updateFilters() and clearFilters()
|
|
916
1002
|
// to avoid relying on watch reactivity which can be unreliable with object mutations
|
|
917
1003
|
|
|
1004
|
+
// ============ LIST:ALTER HOOK ============
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Invoke list:alter hooks to allow modules to modify list configuration
|
|
1008
|
+
*
|
|
1009
|
+
* Builds a config snapshot from current state, passes it through the hook chain,
|
|
1010
|
+
* and applies any modifications back to the internal maps.
|
|
1011
|
+
*
|
|
1012
|
+
* Hook context structure:
|
|
1013
|
+
* @typedef {object} ListAlterConfig
|
|
1014
|
+
* @property {string} entity - Entity name
|
|
1015
|
+
* @property {Array} columns - Array of column definitions
|
|
1016
|
+
* @property {Array} filters - Array of filter definitions
|
|
1017
|
+
* @property {Array} actions - Array of row action definitions
|
|
1018
|
+
* @property {Array} headerActions - Array of header action definitions
|
|
1019
|
+
*
|
|
1020
|
+
* @example
|
|
1021
|
+
* // Register a hook to add a custom column
|
|
1022
|
+
* hooks.register('list:alter', (config) => {
|
|
1023
|
+
* if (config.entity === 'books') {
|
|
1024
|
+
* config.columns.push({ field: 'custom', header: 'Custom' })
|
|
1025
|
+
* }
|
|
1026
|
+
* return config
|
|
1027
|
+
* })
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* // Register entity-specific hook
|
|
1031
|
+
* hooks.register('books:list:alter', (config) => {
|
|
1032
|
+
* config.filters.push({ name: 'year', type: 'select', options: [...] })
|
|
1033
|
+
* return config
|
|
1034
|
+
* })
|
|
1035
|
+
*/
|
|
1036
|
+
async function invokeListAlterHook() {
|
|
1037
|
+
if (!hooks) return
|
|
1038
|
+
|
|
1039
|
+
// Build config snapshot from current state
|
|
1040
|
+
const configSnapshot = {
|
|
1041
|
+
entity,
|
|
1042
|
+
columns: Array.from(columnsMap.value.values()),
|
|
1043
|
+
filters: Array.from(filtersMap.value.values()),
|
|
1044
|
+
actions: Array.from(actionsMap.value.values()),
|
|
1045
|
+
headerActions: Array.from(headerActionsMap.value.values()),
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Context passed to handlers
|
|
1049
|
+
const hookContext = { entity, manager }
|
|
1050
|
+
|
|
1051
|
+
// Invoke generic list:alter hook
|
|
1052
|
+
let alteredConfig = await hooks.alter('list:alter', configSnapshot, hookContext)
|
|
1053
|
+
|
|
1054
|
+
// Invoke entity-specific hook: {entity}:list:alter
|
|
1055
|
+
const entityHookName = `${entity}:list:alter`
|
|
1056
|
+
if (hooks.hasHook(entityHookName)) {
|
|
1057
|
+
alteredConfig = await hooks.alter(entityHookName, alteredConfig, hookContext)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Apply altered config back to the maps
|
|
1061
|
+
applyAlteredConfig(alteredConfig)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ============ FILTER:ALTER HOOK ============
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Invoke filter:alter hooks to allow modules to modify filter options
|
|
1068
|
+
*
|
|
1069
|
+
* Builds a filters snapshot from current state, passes it through the hook chain,
|
|
1070
|
+
* and applies any modifications back to the filtersMap. Runs after API options are
|
|
1071
|
+
* loaded but before list:alter hook.
|
|
1072
|
+
*
|
|
1073
|
+
* Config object structure passed to handlers:
|
|
1074
|
+
* @typedef {object} FilterAlterConfig
|
|
1075
|
+
* @property {string} entity - Entity name (for conditional logic)
|
|
1076
|
+
* @property {Array<object>} filters - Array of filter definitions with loaded options
|
|
1077
|
+
* @property {string} filters[].name - Filter name/identifier
|
|
1078
|
+
* @property {string} filters[].type - Filter type (select, multiselect, etc.)
|
|
1079
|
+
* @property {Array<{label: string, value: *}>} filters[].options - Available options
|
|
1080
|
+
*
|
|
1081
|
+
* Hook invocation order:
|
|
1082
|
+
* 1. `filter:alter` - Generic hook for all entities
|
|
1083
|
+
* 2. `{entity}:filter:alter` - Entity-specific hook (e.g., `books:filter:alter`)
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* // Add custom filter options dynamically
|
|
1087
|
+
* hooks.register('filter:alter', (config) => {
|
|
1088
|
+
* if (config.entity === 'books') {
|
|
1089
|
+
* const statusFilter = config.filters.find(f => f.name === 'status')
|
|
1090
|
+
* if (statusFilter) {
|
|
1091
|
+
* statusFilter.options.push({ label: 'Archived', value: 'archived' })
|
|
1092
|
+
* }
|
|
1093
|
+
* }
|
|
1094
|
+
* return config
|
|
1095
|
+
* })
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* // Filter options based on user context (entity-specific)
|
|
1099
|
+
* hooks.register('books:filter:alter', (config) => {
|
|
1100
|
+
* const genreFilter = config.filters.find(f => f.name === 'genre')
|
|
1101
|
+
* if (genreFilter) {
|
|
1102
|
+
* // Remove restricted options
|
|
1103
|
+
* genreFilter.options = genreFilter.options.filter(o => o.value !== 'restricted')
|
|
1104
|
+
* }
|
|
1105
|
+
* return config
|
|
1106
|
+
* })
|
|
1107
|
+
*/
|
|
1108
|
+
async function invokeFilterAlterHook() {
|
|
1109
|
+
if (!hooks) return
|
|
1110
|
+
|
|
1111
|
+
// Build filters snapshot from current state
|
|
1112
|
+
// Entity is included in the snapshot for handlers to filter by entity
|
|
1113
|
+
const filterSnapshot = {
|
|
1114
|
+
entity,
|
|
1115
|
+
filters: Array.from(filtersMap.value.values()),
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Invoke generic filter:alter hook
|
|
1119
|
+
let alteredFilters = await hooks.alter('filter:alter', filterSnapshot)
|
|
1120
|
+
|
|
1121
|
+
// Invoke entity-specific hook: {entity}:filter:alter
|
|
1122
|
+
const entityHookName = `${entity}:filter:alter`
|
|
1123
|
+
if (hooks.hasHook(entityHookName)) {
|
|
1124
|
+
alteredFilters = await hooks.alter(entityHookName, alteredFilters)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Apply altered filters back to the map
|
|
1128
|
+
if (alteredFilters.filters) {
|
|
1129
|
+
filtersMap.value.clear()
|
|
1130
|
+
for (const filter of alteredFilters.filters) {
|
|
1131
|
+
filtersMap.value.set(filter.name, filter)
|
|
1132
|
+
// Preserve existing filter values or use defaults
|
|
1133
|
+
if (filterValues.value[filter.name] === undefined) {
|
|
1134
|
+
filterValues.value[filter.name] = filter.default ?? null
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Apply altered configuration back to internal maps
|
|
1142
|
+
* @param {ListAlterConfig} alteredConfig - The modified configuration
|
|
1143
|
+
*/
|
|
1144
|
+
function applyAlteredConfig(alteredConfig) {
|
|
1145
|
+
// Apply columns
|
|
1146
|
+
if (alteredConfig.columns) {
|
|
1147
|
+
columnsMap.value.clear()
|
|
1148
|
+
for (const col of alteredConfig.columns) {
|
|
1149
|
+
columnsMap.value.set(col.field, col)
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Apply filters
|
|
1154
|
+
if (alteredConfig.filters) {
|
|
1155
|
+
filtersMap.value.clear()
|
|
1156
|
+
for (const filter of alteredConfig.filters) {
|
|
1157
|
+
filtersMap.value.set(filter.name, filter)
|
|
1158
|
+
// Preserve existing filter values or use defaults
|
|
1159
|
+
if (filterValues.value[filter.name] === undefined) {
|
|
1160
|
+
filterValues.value[filter.name] = filter.default ?? null
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Apply actions
|
|
1166
|
+
if (alteredConfig.actions) {
|
|
1167
|
+
actionsMap.value.clear()
|
|
1168
|
+
for (const action of alteredConfig.actions) {
|
|
1169
|
+
actionsMap.value.set(action.name, action)
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Apply header actions
|
|
1174
|
+
if (alteredConfig.headerActions) {
|
|
1175
|
+
headerActionsMap.value.clear()
|
|
1176
|
+
for (const action of alteredConfig.headerActions) {
|
|
1177
|
+
headerActionsMap.value.set(action.name, action)
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
918
1182
|
// ============ LIFECYCLE ============
|
|
919
1183
|
// Initialize from registry immediately (sync)
|
|
920
1184
|
initFromRegistry()
|
|
@@ -929,6 +1193,10 @@ export function useListPageBuilder(config = {}) {
|
|
|
929
1193
|
await loadFilterOptions()
|
|
930
1194
|
}
|
|
931
1195
|
|
|
1196
|
+
// Invoke list:alter hooks to allow modules to modify configuration
|
|
1197
|
+
// This runs after initFromRegistry() and loadFilterOptions(), before loadItems()
|
|
1198
|
+
await invokeListAlterHook()
|
|
1199
|
+
|
|
932
1200
|
// Load data
|
|
933
1201
|
if (loadOnMount && manager) {
|
|
934
1202
|
loadItems()
|
|
@@ -1022,6 +1290,9 @@ export function useListPageBuilder(config = {}) {
|
|
|
1022
1290
|
// Cards
|
|
1023
1291
|
cards: cards.value,
|
|
1024
1292
|
|
|
1293
|
+
// Columns
|
|
1294
|
+
columns: columns.value,
|
|
1295
|
+
|
|
1025
1296
|
// Table data
|
|
1026
1297
|
items: displayItems.value,
|
|
1027
1298
|
loading: loading.value,
|
|
@@ -1090,6 +1361,12 @@ export function useListPageBuilder(config = {}) {
|
|
|
1090
1361
|
searchConfig,
|
|
1091
1362
|
setSearch,
|
|
1092
1363
|
|
|
1364
|
+
// Columns
|
|
1365
|
+
columns,
|
|
1366
|
+
addColumn,
|
|
1367
|
+
removeColumn,
|
|
1368
|
+
updateColumn,
|
|
1369
|
+
|
|
1093
1370
|
// Header Actions
|
|
1094
1371
|
headerActions,
|
|
1095
1372
|
addHeaderAction,
|
|
@@ -1124,10 +1401,17 @@ export function useListPageBuilder(config = {}) {
|
|
|
1124
1401
|
addAction,
|
|
1125
1402
|
removeAction,
|
|
1126
1403
|
getActions,
|
|
1404
|
+
getRowActions, // Permission-aware alias for getActions
|
|
1127
1405
|
addViewAction,
|
|
1128
1406
|
addEditAction,
|
|
1129
1407
|
addDeleteAction,
|
|
1130
1408
|
|
|
1409
|
+
// Permissions
|
|
1410
|
+
canCreate,
|
|
1411
|
+
canDelete,
|
|
1412
|
+
canEditRow,
|
|
1413
|
+
canDeleteRow,
|
|
1414
|
+
|
|
1131
1415
|
// Data
|
|
1132
1416
|
loadItems,
|
|
1133
1417
|
|