@quazardous/qdadm 2.4.3 → 2.4.5
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/package.json +1 -1
- package/src/composables/useDirtyState.ts +33 -20
- package/src/composables/useListPage.alterHooks.ts +153 -0
- package/src/composables/useListPage.filters.ts +421 -0
- package/src/composables/useListPage.ts +52 -412
- package/src/entity/EntityManager.ts +1 -4
- package/src/entity/auth/EntityAuthAdapter.ts +10 -0
- package/src/entity/auth/SecurityChecker.ts +6 -13
- package/src/kernel/Kernel.api.ts +5 -5
- package/src/kernel/Kernel.auth.ts +5 -5
- package/src/kernel/Kernel.i18n.ts +5 -5
- package/src/kernel/Kernel.modules.ts +5 -5
- package/src/kernel/Kernel.registries.ts +5 -5
- package/src/kernel/Kernel.routing.ts +5 -5
- package/src/kernel/Kernel.vue.ts +5 -5
- package/src/kernel/KernelContext.entities.ts +5 -5
- package/src/kernel/KernelContext.events.ts +5 -5
- package/src/kernel/KernelContext.i18n.ts +5 -5
- package/src/kernel/KernelContext.permissions.ts +5 -5
- package/src/kernel/KernelContext.routing.ts +5 -5
- package/src/kernel/KernelContext.types.ts +5 -0
- package/src/kernel/KernelContext.zones.ts +5 -5
- package/src/query/FilterQuery.ts +1 -1
|
@@ -33,7 +33,6 @@ import { useConfirm } from 'primevue/useconfirm'
|
|
|
33
33
|
import { useHooks } from './useHooks.js'
|
|
34
34
|
import { useEntityItemPage, type ParentConfig, type UseEntityItemPageReturn } from './useEntityItemPage.js'
|
|
35
35
|
import { useActiveStack } from '../chain/useActiveStack.js'
|
|
36
|
-
import { FilterQuery } from '../query/FilterQuery'
|
|
37
36
|
import { useI18n } from '../i18n/useI18n'
|
|
38
37
|
import { formatDateOnly } from '../utils/formatters'
|
|
39
38
|
|
|
@@ -85,17 +84,15 @@ export type {
|
|
|
85
84
|
// Stateless utilities (cookies, session storage, formatters, constants).
|
|
86
85
|
import {
|
|
87
86
|
PAGE_SIZE_OPTIONS,
|
|
88
|
-
SMART_FILTER_THRESHOLD,
|
|
89
|
-
clearSessionFilters,
|
|
90
87
|
getSavedPageSize,
|
|
91
88
|
getSessionFilters,
|
|
92
89
|
getSessionSort,
|
|
93
90
|
setSessionSort,
|
|
94
91
|
persistPageSize,
|
|
95
|
-
setSessionFilters,
|
|
96
|
-
snakeToTitle,
|
|
97
92
|
} from './useListPage.utils'
|
|
98
93
|
import { useActionRegistry } from './useActionRegistry'
|
|
94
|
+
import { useListFilters } from './useListPage.filters'
|
|
95
|
+
import { useListAlterHooks } from './useListPage.alterHooks'
|
|
99
96
|
import { useOrchestrator } from '../orchestrator/useOrchestrator.js'
|
|
100
97
|
import { createOrchestratorToast } from './useOrchestratorToast'
|
|
101
98
|
|
|
@@ -514,323 +511,45 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
514
511
|
|
|
515
512
|
const cards = computed(() => Array.from(cardsMap.value.values()))
|
|
516
513
|
|
|
517
|
-
// ============ FILTERS ============
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
function setFilterValue(name: string, value: unknown): void {
|
|
539
|
-
filterValues.value = { ...filterValues.value, [name]: value }
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function updateFilters(newValues: Record<string, unknown>): void {
|
|
543
|
-
filterValues.value = { ...filterValues.value, ...newValues }
|
|
544
|
-
onFiltersChanged()
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function onFiltersChanged(): void {
|
|
548
|
-
page.value = 1
|
|
549
|
-
loadItems()
|
|
550
|
-
if (persistFilters) {
|
|
551
|
-
const toPersist: Record<string, unknown> = {}
|
|
552
|
-
for (const [name, value] of Object.entries(filterValues.value)) {
|
|
553
|
-
const filterDef = filtersMap.value.get(name)
|
|
554
|
-
if (
|
|
555
|
-
filterDef?.persist !== false &&
|
|
556
|
-
value !== null &&
|
|
557
|
-
value !== undefined &&
|
|
558
|
-
value !== ''
|
|
559
|
-
) {
|
|
560
|
-
toPersist[name] = value
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (searchQuery.value) {
|
|
564
|
-
toPersist._search = searchQuery.value
|
|
565
|
-
}
|
|
566
|
-
setSessionFilters(filterSessionKey, toPersist)
|
|
567
|
-
}
|
|
568
|
-
if (syncUrlParams) {
|
|
569
|
-
const query = { ...route.query } as Record<string, string>
|
|
570
|
-
for (const [name, value] of Object.entries(filterValues.value)) {
|
|
571
|
-
if (value !== null && value !== undefined && value !== '') {
|
|
572
|
-
query[name] = String(value)
|
|
573
|
-
} else {
|
|
574
|
-
delete query[name]
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
if (searchQuery.value) {
|
|
578
|
-
query.search = searchQuery.value
|
|
579
|
-
} else {
|
|
580
|
-
delete query.search
|
|
581
|
-
}
|
|
582
|
-
router.replace({ query })
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function clearFilters(): void {
|
|
587
|
-
const cleared: Record<string, unknown> = {}
|
|
588
|
-
for (const [key, filterDef] of filtersMap.value.entries()) {
|
|
589
|
-
cleared[key] = filterDef.default ?? null
|
|
590
|
-
}
|
|
591
|
-
filterValues.value = cleared
|
|
592
|
-
searchQuery.value = ''
|
|
593
|
-
if (persistFilters) {
|
|
594
|
-
clearSessionFilters(filterSessionKey)
|
|
595
|
-
}
|
|
596
|
-
if (syncUrlParams) {
|
|
597
|
-
const query = { ...route.query } as Record<string, string>
|
|
598
|
-
for (const key of filtersMap.value.keys()) {
|
|
599
|
-
delete query[key]
|
|
600
|
-
}
|
|
601
|
-
delete query.search
|
|
602
|
-
router.replace({ query })
|
|
603
|
-
}
|
|
604
|
-
page.value = 1
|
|
605
|
-
loadItems()
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const filters = computed(() => Array.from(filtersMap.value.values()))
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Check if a filter is at its default value
|
|
612
|
-
* Used for styling: default = blue (info), modified = orange (warning)
|
|
613
|
-
*/
|
|
614
|
-
function isFilterAtDefault(name: string): boolean {
|
|
615
|
-
const filterDef = filtersMap.value.get(name)
|
|
616
|
-
if (!filterDef) return true
|
|
617
|
-
const currentValue = filterValues.value[name]
|
|
618
|
-
const defaultValue = filterDef.default ?? null
|
|
619
|
-
return currentValue === defaultValue
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Check if any filter is NOT at its default value (or search is active)
|
|
624
|
-
* Useful to show a "clear filters" button only when needed
|
|
625
|
-
*/
|
|
626
|
-
const hasActiveFilters = computed(() => {
|
|
627
|
-
if (searchQuery.value) return true
|
|
628
|
-
for (const [name, filterDef] of filtersMap.value.entries()) {
|
|
629
|
-
const currentValue = filterValues.value[name]
|
|
630
|
-
const defaultValue = filterDef.default ?? null
|
|
631
|
-
if (currentValue !== defaultValue) return true
|
|
632
|
-
}
|
|
633
|
-
return false
|
|
514
|
+
// ============ FILTERS (#1195 — extracted subsystem) ============
|
|
515
|
+
// invokeFilterAlterHook is a thunk: alterHooks is declared after the
|
|
516
|
+
// actions section (it needs the action registry) and only runs post-setup.
|
|
517
|
+
const listFilters = useListFilters({
|
|
518
|
+
entityName,
|
|
519
|
+
manager,
|
|
520
|
+
orchestrator,
|
|
521
|
+
items: items as Ref<unknown[]>,
|
|
522
|
+
page,
|
|
523
|
+
searchQuery,
|
|
524
|
+
route,
|
|
525
|
+
router,
|
|
526
|
+
savedFilters,
|
|
527
|
+
persistFilters,
|
|
528
|
+
syncUrlParams,
|
|
529
|
+
autoLoadFilters,
|
|
530
|
+
filterSessionKey,
|
|
531
|
+
entityFilters,
|
|
532
|
+
loadItems: () => loadItems(),
|
|
533
|
+
setSearch: (searchCfg) => setSearch(searchCfg),
|
|
534
|
+
invokeFilterAlterHook: () => alterHooks.invokeFilterAlterHook(),
|
|
634
535
|
})
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
async function loadFilterOptions(): Promise<void> {
|
|
656
|
-
// Process filters configured directly via addFilter() (smart filter modes)
|
|
657
|
-
for (const [filterName, filterDef] of filtersMap.value) {
|
|
658
|
-
if (filterDef.options && filterDef.options.length > 1) continue
|
|
659
|
-
if (filterDef.optionsFromCache) continue
|
|
660
|
-
|
|
661
|
-
try {
|
|
662
|
-
let rawOptions: Array<{ label: string; value: unknown }> | null = null
|
|
663
|
-
|
|
664
|
-
// Mode 1: optionsEntity - fetch from related EntityManager via FilterQuery
|
|
665
|
-
if (filterDef.optionsEntity) {
|
|
666
|
-
const filterQuery = new FilterQuery({
|
|
667
|
-
source: 'entity',
|
|
668
|
-
entity: filterDef.optionsEntity,
|
|
669
|
-
label: filterDef.optionLabel || 'name',
|
|
670
|
-
value: filterDef.optionValue || 'id',
|
|
671
|
-
})
|
|
672
|
-
|
|
673
|
-
rawOptions = (await filterQuery.getOptions(orchestrator)) as Array<{
|
|
674
|
-
label: string
|
|
675
|
-
value: unknown
|
|
676
|
-
}>
|
|
677
|
-
filterDef._filterQuery = filterQuery
|
|
678
|
-
}
|
|
679
|
-
// Mode 2: optionsEndpoint - fetch from API endpoint
|
|
680
|
-
else if (filterDef.optionsEndpoint) {
|
|
681
|
-
const endpoint =
|
|
682
|
-
filterDef.optionsEndpoint === true
|
|
683
|
-
? `distinct/${filterName}`
|
|
684
|
-
: filterDef.optionsEndpoint
|
|
685
|
-
const response = await manager.request('GET', endpoint as string)
|
|
686
|
-
const data = Array.isArray(response)
|
|
687
|
-
? response
|
|
688
|
-
: ((response as Record<string, unknown>)?.items as unknown[]) || []
|
|
689
|
-
rawOptions = data.map((opt) => {
|
|
690
|
-
if (typeof opt === 'object' && opt !== null) {
|
|
691
|
-
const o = opt as Record<string, unknown>
|
|
692
|
-
return {
|
|
693
|
-
label: (o.label || o.name || String(o.value ?? o.id)) as string,
|
|
694
|
-
value: o.value ?? o.id,
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
return { label: snakeToTitle(String(opt)), value: opt }
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (rawOptions !== null) {
|
|
702
|
-
const cacheOptions = filterDef.cacheOptions ?? 'auto'
|
|
703
|
-
let shouldCache = cacheOptions === true
|
|
704
|
-
|
|
705
|
-
if (cacheOptions === 'auto') {
|
|
706
|
-
shouldCache = rawOptions.length <= SMART_FILTER_THRESHOLD
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const componentType =
|
|
710
|
-
filterDef.component || (shouldCache ? 'dropdown' : 'autocomplete')
|
|
711
|
-
const allLabel =
|
|
712
|
-
filterDef.allLabel || filterDef.placeholder || `All ${snakeToTitle(filterName)}`
|
|
713
|
-
let finalOptions = [{ label: allLabel, value: null as unknown }, ...rawOptions]
|
|
714
|
-
|
|
715
|
-
if (typeof filterDef.processor === 'function') {
|
|
716
|
-
finalOptions = filterDef.processor(finalOptions)
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const updatedFilter: FilterConfig = {
|
|
720
|
-
...filterDef,
|
|
721
|
-
options: finalOptions,
|
|
722
|
-
type: componentType,
|
|
723
|
-
_cacheOptions: shouldCache,
|
|
724
|
-
_optionsLoaded: shouldCache,
|
|
725
|
-
}
|
|
726
|
-
delete updatedFilter.optionLabel
|
|
727
|
-
delete updatedFilter.optionValue
|
|
728
|
-
filtersMap.value.set(filterName, updatedFilter)
|
|
729
|
-
}
|
|
730
|
-
} catch (error) {
|
|
731
|
-
console.warn(`[qdadm] Failed to load options for filter "${filterName}":`, error)
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Invoke filter:alter hooks after all options are loaded
|
|
736
|
-
await invokeFilterAlterHook()
|
|
737
|
-
|
|
738
|
-
// Trigger Vue reactivity
|
|
739
|
-
filtersMap.value = new Map(filtersMap.value)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function updateCacheBasedFilters(): Promise<void> {
|
|
743
|
-
if (items.value.length === 0) return
|
|
744
|
-
|
|
745
|
-
let hasChanges = false
|
|
746
|
-
|
|
747
|
-
for (const [filterName, filterDef] of filtersMap.value) {
|
|
748
|
-
if (!filterDef.optionsFromCache) continue
|
|
749
|
-
if (filterDef._optionsLoaded) continue
|
|
750
|
-
// Skip if filter has an explicit query property (advanced usage)
|
|
751
|
-
if (filterDef.query) continue
|
|
752
|
-
|
|
753
|
-
const currentValue = filterValues.value[filterName]
|
|
754
|
-
if (currentValue !== null && currentValue !== undefined && currentValue !== '') {
|
|
755
|
-
continue
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const fieldName =
|
|
759
|
-
typeof filterDef.optionsFromCache === 'string' ? filterDef.optionsFromCache : filterName
|
|
760
|
-
|
|
761
|
-
const filterQuery = new FilterQuery({
|
|
762
|
-
source: 'field',
|
|
763
|
-
field: fieldName,
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
// Provide a minimal mock manager with cached data for field-based filtering
|
|
767
|
-
filterQuery.setParentManager({
|
|
768
|
-
_cache: items.value,
|
|
769
|
-
list: async () => ({ items: items.value }),
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
const rawOptions = (await filterQuery.getOptions()) as Array<{ label: string; value: unknown }>
|
|
773
|
-
|
|
774
|
-
const cacheOptions = filterDef.cacheOptions ?? 'auto'
|
|
775
|
-
let shouldCache = true
|
|
776
|
-
|
|
777
|
-
if (cacheOptions === 'auto') {
|
|
778
|
-
shouldCache = rawOptions.length <= SMART_FILTER_THRESHOLD
|
|
779
|
-
} else if (cacheOptions === false) {
|
|
780
|
-
shouldCache = false
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const componentType =
|
|
784
|
-
filterDef.component ||
|
|
785
|
-
(rawOptions.length <= SMART_FILTER_THRESHOLD ? 'dropdown' : 'autocomplete')
|
|
786
|
-
|
|
787
|
-
const allLabel =
|
|
788
|
-
filterDef.allLabel || filterDef.placeholder || `All ${snakeToTitle(filterName)}`
|
|
789
|
-
let finalOptions = [
|
|
790
|
-
{ label: allLabel, value: null as unknown },
|
|
791
|
-
...rawOptions.map((opt) => ({
|
|
792
|
-
label: snakeToTitle(String(opt.label)),
|
|
793
|
-
value: opt.value,
|
|
794
|
-
})),
|
|
795
|
-
]
|
|
796
|
-
|
|
797
|
-
if (typeof filterDef.processor === 'function') {
|
|
798
|
-
finalOptions = filterDef.processor(finalOptions)
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const updatedFilter: FilterConfig = {
|
|
802
|
-
...filterDef,
|
|
803
|
-
options: finalOptions,
|
|
804
|
-
type: componentType,
|
|
805
|
-
_cacheOptions: shouldCache,
|
|
806
|
-
_optionsLoaded: true,
|
|
807
|
-
_filterQuery: filterQuery,
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
filtersMap.value.set(filterName, updatedFilter)
|
|
811
|
-
hasChanges = true
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (hasChanges) {
|
|
815
|
-
filtersMap.value = new Map(filtersMap.value)
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function restoreFilters(): void {
|
|
820
|
-
for (const key of filtersMap.value.keys()) {
|
|
821
|
-
if (route.query[key] !== undefined) {
|
|
822
|
-
let value: unknown = route.query[key]
|
|
823
|
-
if (value === 'true') value = true
|
|
824
|
-
else if (value === 'false') value = false
|
|
825
|
-
else if (value === 'null') value = null
|
|
826
|
-
else if (!isNaN(Number(value)) && value !== '') value = Number(value)
|
|
827
|
-
filterValues.value[key] = value
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
if (route.query.search) {
|
|
831
|
-
searchQuery.value = route.query.search as string
|
|
832
|
-
}
|
|
833
|
-
}
|
|
536
|
+
const {
|
|
537
|
+
filtersMap,
|
|
538
|
+
filterValues,
|
|
539
|
+
filters,
|
|
540
|
+
hasActiveFilters,
|
|
541
|
+
addFilter,
|
|
542
|
+
removeFilter,
|
|
543
|
+
setFilterValue,
|
|
544
|
+
updateFilters,
|
|
545
|
+
onFiltersChanged,
|
|
546
|
+
clearFilters,
|
|
547
|
+
isFilterAtDefault,
|
|
548
|
+
initFromRegistry,
|
|
549
|
+
loadFilterOptions,
|
|
550
|
+
updateCacheBasedFilters,
|
|
551
|
+
restoreFilters,
|
|
552
|
+
} = listFilters
|
|
834
553
|
|
|
835
554
|
// ============ ACTIONS ============
|
|
836
555
|
function resolveValue<T>(value: T | ((row: unknown) => T), row: unknown): T {
|
|
@@ -852,7 +571,6 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
852
571
|
}
|
|
853
572
|
},
|
|
854
573
|
})
|
|
855
|
-
const actionsMap = actionRegistry.actionsMap
|
|
856
574
|
|
|
857
575
|
function addAction(name: string, actionConfig: Omit<ActionConfig, 'name'>): void {
|
|
858
576
|
actionRegistry.add({ name, ...actionConfig } as ActionConfig)
|
|
@@ -1215,96 +933,18 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
1215
933
|
}
|
|
1216
934
|
})
|
|
1217
935
|
|
|
1218
|
-
// ============ HOOKS ============
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
// Include entity context in the snapshot for hooks
|
|
1232
|
-
const configWithContext = { ...configSnapshot, entity, manager }
|
|
1233
|
-
|
|
1234
|
-
let alteredConfig = await hooks.alter('list:alter', configWithContext)
|
|
1235
|
-
|
|
1236
|
-
const entityHookName = `${entity}:list:alter`
|
|
1237
|
-
if (hooks.hasHook(entityHookName)) {
|
|
1238
|
-
alteredConfig = await hooks.alter(entityHookName, alteredConfig)
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
applyAlteredConfig(alteredConfig)
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
async function invokeFilterAlterHook(): Promise<void> {
|
|
1245
|
-
if (!hooks) return
|
|
1246
|
-
|
|
1247
|
-
const filterSnapshot = {
|
|
1248
|
-
entity,
|
|
1249
|
-
filters: Array.from(filtersMap.value.values()),
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
let alteredFilters = (await hooks.alter('filter:alter', filterSnapshot)) as typeof filterSnapshot
|
|
1253
|
-
|
|
1254
|
-
const entityHookName = `${entity}:filter:alter`
|
|
1255
|
-
if (hooks.hasHook(entityHookName)) {
|
|
1256
|
-
alteredFilters = (await hooks.alter(entityHookName, alteredFilters)) as typeof filterSnapshot
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
if (alteredFilters.filters) {
|
|
1260
|
-
filtersMap.value.clear()
|
|
1261
|
-
for (const filter of alteredFilters.filters) {
|
|
1262
|
-
filtersMap.value.set(filter.name, filter)
|
|
1263
|
-
if (filterValues.value[filter.name] === undefined) {
|
|
1264
|
-
filterValues.value[filter.name] = filter.default ?? null
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function applyAlteredConfig(alteredConfig: {
|
|
1271
|
-
columns?: ColumnConfig[]
|
|
1272
|
-
filters?: FilterConfig[]
|
|
1273
|
-
actions?: ActionConfig[]
|
|
1274
|
-
headerActions?: HeaderActionConfig[]
|
|
1275
|
-
}): void {
|
|
1276
|
-
if (alteredConfig.columns) {
|
|
1277
|
-
columnsMap.value.clear()
|
|
1278
|
-
for (const col of alteredConfig.columns) {
|
|
1279
|
-
columnsMap.value.set(col.field, col)
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
if (alteredConfig.filters) {
|
|
1284
|
-
filtersMap.value.clear()
|
|
1285
|
-
for (const filter of alteredConfig.filters) {
|
|
1286
|
-
filtersMap.value.set(filter.name, filter)
|
|
1287
|
-
if (filterValues.value[filter.name] === undefined) {
|
|
1288
|
-
filterValues.value[filter.name] = filter.default ?? null
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (alteredConfig.actions) {
|
|
1294
|
-
// Route through the registry so the order list stays in sync (#1193)
|
|
1295
|
-
actionRegistry.clear()
|
|
1296
|
-
for (const action of alteredConfig.actions) {
|
|
1297
|
-
actionRegistry.add(action)
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
if (alteredConfig.headerActions) {
|
|
1302
|
-
headerActionsMap.value.clear()
|
|
1303
|
-
for (const action of alteredConfig.headerActions) {
|
|
1304
|
-
headerActionsMap.value.set(action.name, action)
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
936
|
+
// ============ HOOKS (#1195 — extracted subsystem) ============
|
|
937
|
+
const alterHooks = useListAlterHooks({
|
|
938
|
+
hooks,
|
|
939
|
+
entity,
|
|
940
|
+
manager,
|
|
941
|
+
columnsMap,
|
|
942
|
+
filtersMap,
|
|
943
|
+
filterValues,
|
|
944
|
+
headerActionsMap,
|
|
945
|
+
actionRegistry,
|
|
946
|
+
})
|
|
947
|
+
const { invokeListAlterHook } = alterHooks
|
|
1308
948
|
|
|
1309
949
|
// ============ LIFECYCLE ============
|
|
1310
950
|
initFromRegistry()
|
|
@@ -501,10 +501,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
501
501
|
* Check if SecurityChecker is configured via authAdapter
|
|
502
502
|
*/
|
|
503
503
|
protected _hasSecurityChecker(): boolean {
|
|
504
|
-
return (
|
|
505
|
-
(this.authAdapter as unknown as { _securityChecker?: unknown })
|
|
506
|
-
?._securityChecker != null
|
|
507
|
-
)
|
|
504
|
+
return this.authAdapter?.hasSecurityChecker?.() ?? false
|
|
508
505
|
}
|
|
509
506
|
|
|
510
507
|
/**
|
|
@@ -41,6 +41,16 @@ export class EntityAuthAdapter {
|
|
|
41
41
|
this._securityChecker = checker
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether a SecurityChecker is configured (#1196).
|
|
46
|
+
*
|
|
47
|
+
* Public capability probe — EntityManager used to reach into the
|
|
48
|
+
* private _securityChecker field through a cast.
|
|
49
|
+
*/
|
|
50
|
+
hasSecurityChecker(): boolean {
|
|
51
|
+
return this._securityChecker != null
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* Check if current user is granted an attribute (Symfony-like contract)
|
|
46
56
|
*
|
|
@@ -52,28 +52,24 @@ export interface SecurityCheckerConfig {
|
|
|
52
52
|
*/
|
|
53
53
|
export class SecurityChecker {
|
|
54
54
|
protected _rolesProvider: RoleProvider
|
|
55
|
-
protected _legacyRoleHierarchy: RoleHierarchy | null
|
|
56
55
|
readonly getCurrentUser: () => AuthUser | null
|
|
57
56
|
|
|
58
57
|
constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
|
|
59
|
-
// Support both new rolesProvider and legacy rolePermissions
|
|
60
58
|
if (rolesProvider) {
|
|
61
59
|
this._rolesProvider = rolesProvider
|
|
62
|
-
this._legacyRoleHierarchy = null
|
|
63
60
|
} else {
|
|
64
|
-
// Legacy
|
|
61
|
+
// Legacy options (roleHierarchy/rolePermissions) are normalized ONCE
|
|
62
|
+
// into a static provider (#1196) — the parallel _legacyRoleHierarchy
|
|
63
|
+
// branch is gone; only the provider path remains.
|
|
65
64
|
const hierarchyConfig =
|
|
66
|
-
roleHierarchy instanceof RoleHierarchy
|
|
65
|
+
roleHierarchy instanceof RoleHierarchy
|
|
66
|
+
? roleHierarchy.map
|
|
67
|
+
: (roleHierarchy as RoleHierarchyConfig) || {}
|
|
67
68
|
|
|
68
69
|
this._rolesProvider = new StaticRoleProvider({
|
|
69
70
|
role_hierarchy: hierarchyConfig,
|
|
70
71
|
role_permissions: rolePermissions || {},
|
|
71
72
|
})
|
|
72
|
-
|
|
73
|
-
this._legacyRoleHierarchy =
|
|
74
|
-
roleHierarchy instanceof RoleHierarchy
|
|
75
|
-
? roleHierarchy
|
|
76
|
-
: new RoleHierarchy(hierarchyConfig)
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
this.getCurrentUser = getCurrentUser
|
|
@@ -83,9 +79,6 @@ export class SecurityChecker {
|
|
|
83
79
|
* Get role hierarchy (dynamically resolved from rolesProvider)
|
|
84
80
|
*/
|
|
85
81
|
get roleHierarchy(): RoleHierarchy {
|
|
86
|
-
if (this._legacyRoleHierarchy) {
|
|
87
|
-
return this._legacyRoleHierarchy
|
|
88
|
-
}
|
|
89
82
|
return new RoleHierarchy(this._rolesProvider.getHierarchy())
|
|
90
83
|
}
|
|
91
84
|
|
package/src/kernel/Kernel.api.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { AxiosLikeClient, AxiosError } from './Kernel.types'
|
|
2
|
+
import type { Kernel } from './Kernel'
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
type Self =
|
|
4
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
5
|
+
type Self = Kernel
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Patch Kernel prototype with API client methods.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const proto = KernelClass.prototype as Self
|
|
10
|
+
export function applyApiMethods(KernelClass: { prototype: Kernel }): void {
|
|
11
|
+
const proto = KernelClass.prototype
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Setup an axios client with automatic auth and error handling
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { authFactory, CompositeAuthAdapter } from '../entity/auth'
|
|
2
|
+
import type { Kernel } from './Kernel'
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
type Self =
|
|
4
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
5
|
+
type Self = Kernel
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Patch Kernel prototype with auth-related methods.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const proto = KernelClass.prototype as Self
|
|
10
|
+
export function applyAuthMethods(KernelClass: { prototype: Kernel }): void {
|
|
11
|
+
const proto = KernelClass.prototype
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Resolve entityAuthAdapter through authFactory
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import { I18n } from '../i18n/I18n'
|
|
11
11
|
import type { I18nOptions } from '../i18n/types'
|
|
12
|
+
import type { Kernel } from './Kernel'
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
type Self =
|
|
14
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
15
|
+
type Self = Kernel
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const proto = KernelClass.prototype as Self
|
|
17
|
+
export function applyI18nMethods(KernelClass: { prototype: Kernel }): void {
|
|
18
|
+
const proto = KernelClass.prototype
|
|
19
19
|
|
|
20
20
|
proto._createI18n = function (this: Self): void {
|
|
21
21
|
const opts = (this.options.i18n as I18nOptions | undefined) ?? {}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { initModules, setSectionOrder } from '../module/moduleRegistry'
|
|
2
2
|
import { createModuleLoader, type ModuleLike } from './ModuleLoader'
|
|
3
3
|
import { createKernelContext, type KernelContext } from './KernelContext'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import type { Kernel } from './Kernel'
|
|
5
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
6
|
+
type Self = Kernel
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Patch Kernel prototype with module-related methods.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const proto = KernelClass.prototype as Self
|
|
11
|
+
export function applyModuleMethods(KernelClass: { prototype: Kernel }): void {
|
|
12
|
+
const proto = KernelClass.prototype
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Fire entity cache warmups
|
|
@@ -13,15 +13,15 @@ import { ActiveStack } from '../chain/ActiveStack.js'
|
|
|
13
13
|
import { StackHydrator } from '../chain/StackHydrator.js'
|
|
14
14
|
import { Orchestrator } from '../orchestrator/Orchestrator'
|
|
15
15
|
import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import type { Kernel } from './Kernel'
|
|
17
|
+
// #1196 Phase B — this-typing against the real Kernel shape (was Self = any)
|
|
18
|
+
type Self = Kernel
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Patch Kernel prototype with registry-related methods.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const proto = KernelClass.prototype as Self
|
|
23
|
+
export function applyRegistryMethods(KernelClass: { prototype: Kernel }): void {
|
|
24
|
+
const proto = KernelClass.prototype
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Create signal bus for event-driven communication.
|