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