@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,558 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PermissionEditor - Fragmented autocomplete for permissions
|
|
4
|
+
*
|
|
5
|
+
* Single text input with segment-aware completion:
|
|
6
|
+
* - Type "au" → suggests "auth"
|
|
7
|
+
* - Tab/select → completes to "auth:"
|
|
8
|
+
* - Then suggests actions for that namespace
|
|
9
|
+
*
|
|
10
|
+
* Permission format: namespace:action
|
|
11
|
+
* - entity:books:read (namespace=entity:books, action=read)
|
|
12
|
+
* - auth:impersonate (namespace=auth, action=impersonate)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ref, computed, nextTick, type ComponentPublicInstance } from 'vue'
|
|
16
|
+
import AutoComplete from 'primevue/autocomplete'
|
|
17
|
+
import Chip from 'primevue/chip'
|
|
18
|
+
|
|
19
|
+
interface Permission {
|
|
20
|
+
key: string
|
|
21
|
+
namespace: string
|
|
22
|
+
action: string
|
|
23
|
+
label: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PermissionRegistry {
|
|
27
|
+
getAll(): Permission[]
|
|
28
|
+
get(key: string): Permission | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Suggestion {
|
|
32
|
+
label: string
|
|
33
|
+
value: string
|
|
34
|
+
type: 'namespace' | 'permission' | 'wildcard' | 'wildcard-action'
|
|
35
|
+
description: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PermissionInfo {
|
|
39
|
+
namespace: string
|
|
40
|
+
action: string
|
|
41
|
+
label: string
|
|
42
|
+
isWildcard?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Props {
|
|
46
|
+
modelValue?: string[]
|
|
47
|
+
disabled?: boolean
|
|
48
|
+
permissionRegistry?: PermissionRegistry | null
|
|
49
|
+
placeholder?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
53
|
+
modelValue: () => [],
|
|
54
|
+
disabled: false,
|
|
55
|
+
placeholder: 'Type permission...'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const emit = defineEmits<{
|
|
59
|
+
'update:modelValue': [value: string[]]
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
// Current input value
|
|
63
|
+
const inputValue = ref<string>('')
|
|
64
|
+
const suggestions = ref<Suggestion[]>([])
|
|
65
|
+
const autocompleteRef = ref<ComponentPublicInstance | null>(null)
|
|
66
|
+
|
|
67
|
+
// Get all permissions from registry
|
|
68
|
+
const allPermissions = computed<Permission[]>(() => {
|
|
69
|
+
if (!props.permissionRegistry) {
|
|
70
|
+
console.warn('[PermissionEditor] No permissionRegistry provided')
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
const perms = props.permissionRegistry.getAll()
|
|
74
|
+
return perms
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Check if registry is available
|
|
78
|
+
const hasRegistry = computed<boolean>(() => {
|
|
79
|
+
return !!props.permissionRegistry && allPermissions.value.length > 0
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Get all permission keys
|
|
83
|
+
const allPermissionKeys = computed<string[]>(() => {
|
|
84
|
+
return allPermissions.value.map(p => p.key)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Get all unique prefixes at each level
|
|
88
|
+
// e.g., from "entity:books:read" we get ["entity", "entity:books", "entity:books:read"]
|
|
89
|
+
const allPrefixes = computed<string[]>(() => {
|
|
90
|
+
const prefixes = new Set<string>()
|
|
91
|
+
for (const key of allPermissionKeys.value) {
|
|
92
|
+
const parts = key.split(':')
|
|
93
|
+
let current = ''
|
|
94
|
+
for (let i = 0; i < parts.length; i++) {
|
|
95
|
+
const part = parts[i]
|
|
96
|
+
if (part !== undefined) {
|
|
97
|
+
current = current ? current + ':' + part : part
|
|
98
|
+
prefixes.add(current)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...prefixes].sort()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Get unique actions across all namespaces
|
|
106
|
+
const allActions = computed<string[]>(() => {
|
|
107
|
+
const actions = new Set<string>()
|
|
108
|
+
for (const perm of allPermissions.value) {
|
|
109
|
+
actions.add(perm.action)
|
|
110
|
+
}
|
|
111
|
+
return [...actions].sort()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate suggestions based on current input
|
|
116
|
+
* Supports multi-level fragment completion:
|
|
117
|
+
* - "ent" → "entity:"
|
|
118
|
+
* - "entity:bo" → "entity:books:"
|
|
119
|
+
* - "entity:books:re" → "entity:books:read"
|
|
120
|
+
*/
|
|
121
|
+
function searchSuggestions(event: { query?: string }): void {
|
|
122
|
+
const query = event.query ?? ''
|
|
123
|
+
const queryLower = query.toLowerCase()
|
|
124
|
+
|
|
125
|
+
// Handle super wildcard **
|
|
126
|
+
if (query === '**') {
|
|
127
|
+
suggestions.value = [{
|
|
128
|
+
label: '**',
|
|
129
|
+
value: '**',
|
|
130
|
+
type: 'wildcard',
|
|
131
|
+
description: 'All permissions (super admin)'
|
|
132
|
+
}]
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle wildcard patterns
|
|
137
|
+
if (query.includes('*')) {
|
|
138
|
+
suggestions.value = generateWildcardSuggestions(query)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if query ends with ':' - user wants next level suggestions
|
|
143
|
+
const endsWithColon = query.endsWith(':')
|
|
144
|
+
|
|
145
|
+
const results: Suggestion[] = []
|
|
146
|
+
|
|
147
|
+
// Add wildcard option if at start or after colon
|
|
148
|
+
if (query === '' || endsWithColon) {
|
|
149
|
+
results.push({
|
|
150
|
+
label: query + '*:',
|
|
151
|
+
value: query + '*:',
|
|
152
|
+
type: 'namespace',
|
|
153
|
+
description: 'Wildcard (all at this level)'
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Find matching prefixes that are NEXT level completions
|
|
158
|
+
for (const prefix of allPrefixes.value) {
|
|
159
|
+
const prefixLower = prefix.toLowerCase()
|
|
160
|
+
|
|
161
|
+
if (endsWithColon) {
|
|
162
|
+
// After "entity:", suggest "entity:books", "entity:users", etc.
|
|
163
|
+
if (prefixLower.startsWith(queryLower) && prefix !== query.slice(0, -1)) {
|
|
164
|
+
const isComplete = allPermissionKeys.value.includes(prefix)
|
|
165
|
+
results.push({
|
|
166
|
+
label: isComplete ? prefix : prefix + ':',
|
|
167
|
+
value: isComplete ? prefix : prefix + ':',
|
|
168
|
+
type: isComplete ? 'permission' : 'namespace',
|
|
169
|
+
description: isComplete ? getPermissionLabel(prefix) : `${countChildren(prefix)} sub-items`
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Partial match: "ent" matches "entity", "entity:books:re" matches "entity:books:read"
|
|
174
|
+
if (prefixLower.startsWith(queryLower) || (query === '' && true)) {
|
|
175
|
+
// Only suggest if it's a progression from current input
|
|
176
|
+
if (prefix.toLowerCase() !== queryLower) {
|
|
177
|
+
const isComplete = allPermissionKeys.value.includes(prefix)
|
|
178
|
+
// Find the next colon boundary for namespace suggestions
|
|
179
|
+
const nextColonIdx = prefix.indexOf(':', query.length)
|
|
180
|
+
const suggestionValue = nextColonIdx > -1 ? prefix.slice(0, nextColonIdx + 1) : (isComplete ? prefix : prefix + ':')
|
|
181
|
+
|
|
182
|
+
// Avoid duplicates
|
|
183
|
+
if (!results.some(r => r.value === suggestionValue)) {
|
|
184
|
+
results.push({
|
|
185
|
+
label: suggestionValue,
|
|
186
|
+
value: suggestionValue,
|
|
187
|
+
type: suggestionValue.endsWith(':') ? 'namespace' : 'permission',
|
|
188
|
+
description: isComplete && !suggestionValue.endsWith(':') ? getPermissionLabel(prefix) : `${countChildren(suggestionValue.replace(/:$/, ''))} sub-items`
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sort: namespaces first, then permissions
|
|
197
|
+
results.sort((a, b) => {
|
|
198
|
+
if (a.type === 'namespace' && b.type !== 'namespace') return -1
|
|
199
|
+
if (a.type !== 'namespace' && b.type === 'namespace') return 1
|
|
200
|
+
return a.label.localeCompare(b.label)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
suggestions.value = results.slice(0, 20) // Limit to 20 suggestions
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate wildcard suggestions
|
|
208
|
+
*/
|
|
209
|
+
function generateWildcardSuggestions(query: string): Suggestion[] {
|
|
210
|
+
const results: Suggestion[] = []
|
|
211
|
+
|
|
212
|
+
// *: pattern - suggest actions
|
|
213
|
+
if (query === '*:' || query.startsWith('*:')) {
|
|
214
|
+
const actionPart = query.slice(2).toLowerCase()
|
|
215
|
+
|
|
216
|
+
results.push({
|
|
217
|
+
label: '*:*',
|
|
218
|
+
value: '*:*',
|
|
219
|
+
type: 'wildcard',
|
|
220
|
+
description: 'All permissions'
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
for (const action of allActions.value) {
|
|
224
|
+
if (actionPart === '' || action.toLowerCase().startsWith(actionPart)) {
|
|
225
|
+
results.push({
|
|
226
|
+
label: '*:' + action,
|
|
227
|
+
value: '*:' + action,
|
|
228
|
+
type: 'wildcard-action',
|
|
229
|
+
description: `All ${action} permissions`
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (query === '*') {
|
|
234
|
+
// Just * - suggest *: to continue
|
|
235
|
+
results.push({
|
|
236
|
+
label: '*:',
|
|
237
|
+
value: '*:',
|
|
238
|
+
type: 'namespace',
|
|
239
|
+
description: 'All namespaces (wildcard)'
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return results
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Count children of a prefix
|
|
248
|
+
*/
|
|
249
|
+
function countChildren(prefix: string): number {
|
|
250
|
+
return allPrefixes.value.filter(p => p.startsWith(prefix + ':') || p === prefix).length
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get permission label
|
|
255
|
+
*/
|
|
256
|
+
function getPermissionLabel(key: string): string {
|
|
257
|
+
const perm = props.permissionRegistry?.get(key)
|
|
258
|
+
return perm?.label || key
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Handle selection from dropdown
|
|
263
|
+
*/
|
|
264
|
+
function onSelect(event: { value: Suggestion }): void {
|
|
265
|
+
const selected = event.value
|
|
266
|
+
|
|
267
|
+
if (!selected) return
|
|
268
|
+
|
|
269
|
+
// If selection ends with ':', it's a namespace - continue typing
|
|
270
|
+
if (selected.value.endsWith(':')) {
|
|
271
|
+
inputValue.value = selected.value
|
|
272
|
+
nextTick(() => {
|
|
273
|
+
const input = (autocompleteRef.value as ComponentPublicInstance & { $el: HTMLElement })?.$el?.querySelector('input') as HTMLInputElement | null
|
|
274
|
+
input?.focus()
|
|
275
|
+
// Trigger new search for next level and show dropdown
|
|
276
|
+
searchSuggestions({ query: selected.value })
|
|
277
|
+
// Force show the dropdown with new suggestions
|
|
278
|
+
;(autocompleteRef.value as ComponentPublicInstance & { show: () => void })?.show()
|
|
279
|
+
})
|
|
280
|
+
} else {
|
|
281
|
+
// Complete permission or wildcard - add to list
|
|
282
|
+
if (!props.modelValue.includes(selected.value)) {
|
|
283
|
+
emit('update:modelValue', [...props.modelValue, selected.value])
|
|
284
|
+
}
|
|
285
|
+
inputValue.value = ''
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Handle click on input - show suggestions if input ends with ':'
|
|
291
|
+
* This ensures the dropdown appears when clicking back into the field
|
|
292
|
+
*/
|
|
293
|
+
function onInputClick(): void {
|
|
294
|
+
if (inputValue.value.endsWith(':')) {
|
|
295
|
+
searchSuggestions({ query: inputValue.value })
|
|
296
|
+
nextTick(() => {
|
|
297
|
+
;(autocompleteRef.value as ComponentPublicInstance & { show: () => void })?.show()
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Handle keyboard events for Tab completion
|
|
304
|
+
*/
|
|
305
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
306
|
+
if (event.key === 'Tab' && suggestions.value.length > 0) {
|
|
307
|
+
event.preventDefault()
|
|
308
|
+
// Select first suggestion
|
|
309
|
+
const first = suggestions.value[0]
|
|
310
|
+
if (first) {
|
|
311
|
+
// If it's a namespace, onSelect will show the dropdown for next level
|
|
312
|
+
onSelect({ value: first })
|
|
313
|
+
}
|
|
314
|
+
} else if (event.key === 'Enter' && inputValue.value) {
|
|
315
|
+
event.preventDefault()
|
|
316
|
+
const value = inputValue.value
|
|
317
|
+
|
|
318
|
+
// Check if it's a valid complete permission or wildcard pattern
|
|
319
|
+
if (allPermissionKeys.value.includes(value) || value.includes('*')) {
|
|
320
|
+
if (!props.modelValue.includes(value)) {
|
|
321
|
+
emit('update:modelValue', [...props.modelValue, value])
|
|
322
|
+
}
|
|
323
|
+
inputValue.value = ''
|
|
324
|
+
} else if (suggestions.value.length > 0 && suggestions.value[0]) {
|
|
325
|
+
// Select first suggestion on Enter
|
|
326
|
+
onSelect({ value: suggestions.value[0] })
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Remove a permission
|
|
333
|
+
*/
|
|
334
|
+
function removePermission(permKey: string): void {
|
|
335
|
+
emit('update:modelValue', props.modelValue.filter(p => p !== permKey))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get display info for a permission
|
|
340
|
+
*/
|
|
341
|
+
function getPermissionInfo(permKey: string): PermissionInfo {
|
|
342
|
+
// Handle super wildcard
|
|
343
|
+
if (permKey === '**') {
|
|
344
|
+
return {
|
|
345
|
+
namespace: '',
|
|
346
|
+
action: '**',
|
|
347
|
+
label: 'All permissions',
|
|
348
|
+
isWildcard: true
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Handle wildcard patterns
|
|
353
|
+
if (permKey.includes('*')) {
|
|
354
|
+
const parts = permKey.split(':')
|
|
355
|
+
const action = parts.pop() || ''
|
|
356
|
+
const namespace = parts.join(':')
|
|
357
|
+
return {
|
|
358
|
+
namespace: namespace || '*',
|
|
359
|
+
action,
|
|
360
|
+
label: permKey,
|
|
361
|
+
isWildcard: true
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const perm = props.permissionRegistry?.get(permKey)
|
|
366
|
+
if (perm) {
|
|
367
|
+
return {
|
|
368
|
+
namespace: perm.namespace,
|
|
369
|
+
action: perm.action,
|
|
370
|
+
label: perm.label
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Fallback for unknown permissions
|
|
374
|
+
const parts = permKey.split(':')
|
|
375
|
+
return {
|
|
376
|
+
namespace: parts.slice(0, -1).join(':'),
|
|
377
|
+
action: parts[parts.length - 1] || '',
|
|
378
|
+
label: permKey
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Custom template for suggestions
|
|
384
|
+
*/
|
|
385
|
+
function getSuggestionClass(item: Suggestion): string {
|
|
386
|
+
return item.type === 'namespace' ? 'suggestion-namespace' : 'suggestion-permission'
|
|
387
|
+
}
|
|
388
|
+
</script>
|
|
389
|
+
|
|
390
|
+
<template>
|
|
391
|
+
<div class="permission-editor">
|
|
392
|
+
<!-- Selected permissions as chips -->
|
|
393
|
+
<div v-if="modelValue.length > 0" class="permission-chips">
|
|
394
|
+
<Chip
|
|
395
|
+
v-for="perm in modelValue"
|
|
396
|
+
:key="perm"
|
|
397
|
+
:label="perm"
|
|
398
|
+
removable
|
|
399
|
+
:disabled="disabled"
|
|
400
|
+
@remove="removePermission(perm)"
|
|
401
|
+
:class="['permission-chip', { 'wildcard-chip': getPermissionInfo(perm).isWildcard }]"
|
|
402
|
+
>
|
|
403
|
+
<template #default>
|
|
404
|
+
<template v-if="perm === '**'">
|
|
405
|
+
<span class="chip-wildcard">**</span>
|
|
406
|
+
</template>
|
|
407
|
+
<template v-else-if="getPermissionInfo(perm).namespace">
|
|
408
|
+
<span class="chip-namespace">{{ getPermissionInfo(perm).namespace }}:</span>
|
|
409
|
+
<span :class="['chip-action', { 'chip-wildcard': getPermissionInfo(perm).action === '*' }]">
|
|
410
|
+
{{ getPermissionInfo(perm).action }}
|
|
411
|
+
</span>
|
|
412
|
+
</template>
|
|
413
|
+
<template v-else>
|
|
414
|
+
<span class="chip-action">{{ perm }}</span>
|
|
415
|
+
</template>
|
|
416
|
+
</template>
|
|
417
|
+
</Chip>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<!-- Autocomplete input -->
|
|
421
|
+
<div class="permission-input">
|
|
422
|
+
<template v-if="hasRegistry">
|
|
423
|
+
<AutoComplete
|
|
424
|
+
ref="autocompleteRef"
|
|
425
|
+
v-model="inputValue"
|
|
426
|
+
:suggestions="suggestions"
|
|
427
|
+
optionLabel="label"
|
|
428
|
+
:disabled="disabled"
|
|
429
|
+
:placeholder="placeholder"
|
|
430
|
+
:minLength="0"
|
|
431
|
+
completeOnFocus
|
|
432
|
+
@complete="searchSuggestions"
|
|
433
|
+
@item-select="onSelect"
|
|
434
|
+
@keydown="onKeydown"
|
|
435
|
+
@click="onInputClick"
|
|
436
|
+
dropdown
|
|
437
|
+
class="w-full"
|
|
438
|
+
>
|
|
439
|
+
<template #option="{ option }">
|
|
440
|
+
<div :class="['suggestion-item', getSuggestionClass(option)]">
|
|
441
|
+
<span class="suggestion-label">{{ option.label }}</span>
|
|
442
|
+
<span class="suggestion-desc">{{ option.description }}</span>
|
|
443
|
+
</div>
|
|
444
|
+
</template>
|
|
445
|
+
<template #empty>
|
|
446
|
+
<div class="suggestion-empty">
|
|
447
|
+
No matching permissions
|
|
448
|
+
</div>
|
|
449
|
+
</template>
|
|
450
|
+
</AutoComplete>
|
|
451
|
+
<small class="text-color-secondary mt-1 block">
|
|
452
|
+
Type permission path (e.g., "entity:books:read"), Tab to complete
|
|
453
|
+
</small>
|
|
454
|
+
</template>
|
|
455
|
+
<template v-else>
|
|
456
|
+
<div class="no-registry">
|
|
457
|
+
<i class="pi pi-info-circle"></i>
|
|
458
|
+
<span>No permissions registered in the system</span>
|
|
459
|
+
</div>
|
|
460
|
+
</template>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</template>
|
|
464
|
+
|
|
465
|
+
<style scoped>
|
|
466
|
+
/*
|
|
467
|
+
* Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
|
|
468
|
+
* Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
|
|
469
|
+
*/
|
|
470
|
+
/* Uses global .editor-box pattern */
|
|
471
|
+
.permission-editor {
|
|
472
|
+
border: 1px solid var(--p-surface-200);
|
|
473
|
+
border-radius: 0.5rem;
|
|
474
|
+
padding: 0.75rem;
|
|
475
|
+
background: var(--p-surface-50);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Uses global .editor-chips pattern */
|
|
479
|
+
.permission-chips {
|
|
480
|
+
display: flex;
|
|
481
|
+
flex-wrap: wrap;
|
|
482
|
+
gap: 0.5rem;
|
|
483
|
+
margin-bottom: 0.75rem;
|
|
484
|
+
padding-bottom: 0.75rem;
|
|
485
|
+
border-bottom: 1px solid var(--p-surface-200);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.permission-chip {
|
|
489
|
+
font-family: monospace;
|
|
490
|
+
font-size: 0.875rem;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* .chip-namespace, .chip-action, .chip-wildcard are global (main.css) */
|
|
494
|
+
|
|
495
|
+
.wildcard-chip {
|
|
496
|
+
background: var(--p-orange-50) !important;
|
|
497
|
+
border-color: var(--p-orange-200) !important;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.permission-input {
|
|
501
|
+
display: flex;
|
|
502
|
+
flex-direction: column;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.suggestion-item {
|
|
506
|
+
display: flex;
|
|
507
|
+
justify-content: space-between;
|
|
508
|
+
align-items: center;
|
|
509
|
+
padding: 0.25rem 0;
|
|
510
|
+
gap: 1rem;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.suggestion-namespace .suggestion-label,
|
|
514
|
+
.suggestion-permission .suggestion-label {
|
|
515
|
+
font-family: monospace;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.suggestion-namespace .suggestion-label {
|
|
519
|
+
color: var(--p-primary-600);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.suggestion-permission .suggestion-label {
|
|
523
|
+
font-weight: 500;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.suggestion-desc {
|
|
527
|
+
font-size: 0.75rem;
|
|
528
|
+
color: var(--p-surface-500);
|
|
529
|
+
white-space: nowrap;
|
|
530
|
+
overflow: hidden;
|
|
531
|
+
text-overflow: ellipsis;
|
|
532
|
+
max-width: 200px;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.suggestion-empty {
|
|
536
|
+
padding: 0.5rem;
|
|
537
|
+
color: var(--p-surface-400);
|
|
538
|
+
font-style: italic;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.no-registry {
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 0.5rem;
|
|
545
|
+
padding: 0.75rem;
|
|
546
|
+
color: var(--p-surface-500);
|
|
547
|
+
font-style: italic;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
:deep(.p-autocomplete) {
|
|
551
|
+
width: 100%;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
:deep(.p-autocomplete-input) {
|
|
555
|
+
width: 100%;
|
|
556
|
+
font-family: monospace;
|
|
557
|
+
}
|
|
558
|
+
</style>
|