@ngbplatform/ui 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/package.json +61 -0
  4. package/public/favicon.svg +6 -0
  5. package/public/silent-check-sso.html +8 -0
  6. package/src/assets/ngb_logo.svg +17 -0
  7. package/src/assets/ngb_logo_color.svg +17 -0
  8. package/src/index.ts +1023 -0
  9. package/src/ngb/README.md +27 -0
  10. package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
  11. package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
  12. package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
  13. package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
  14. package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
  15. package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
  16. package/src/ngb/accounting/api.ts +79 -0
  17. package/src/ngb/accounting/generalJournalEntry.ts +127 -0
  18. package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
  19. package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
  20. package/src/ngb/accounting/navigation.ts +83 -0
  21. package/src/ngb/accounting/periodClosing.ts +75 -0
  22. package/src/ngb/accounting/periodClosingApi.ts +60 -0
  23. package/src/ngb/accounting/periodClosingTypes.ts +73 -0
  24. package/src/ngb/accounting/types.ts +57 -0
  25. package/src/ngb/api/audit.ts +23 -0
  26. package/src/ngb/api/catalogs.ts +50 -0
  27. package/src/ngb/api/contracts.ts +285 -0
  28. package/src/ngb/api/documents.ts +170 -0
  29. package/src/ngb/api/http.ts +446 -0
  30. package/src/ngb/api/lookups.ts +14 -0
  31. package/src/ngb/api/types.ts +6 -0
  32. package/src/ngb/auth/index.ts +13 -0
  33. package/src/ngb/auth/keycloak.ts +374 -0
  34. package/src/ngb/auth/router.ts +35 -0
  35. package/src/ngb/auth/types.ts +12 -0
  36. package/src/ngb/auth/useAuthStore.ts +209 -0
  37. package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
  38. package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
  39. package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
  40. package/src/ngb/command-palette/api.ts +10 -0
  41. package/src/ngb/command-palette/config.ts +16 -0
  42. package/src/ngb/command-palette/presentation.ts +85 -0
  43. package/src/ngb/command-palette/search.ts +130 -0
  44. package/src/ngb/command-palette/staticItems.ts +243 -0
  45. package/src/ngb/command-palette/storage.ts +45 -0
  46. package/src/ngb/command-palette/store.ts +716 -0
  47. package/src/ngb/command-palette/types.ts +120 -0
  48. package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
  49. package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
  50. package/src/ngb/components/NgbConfirmDialog.vue +63 -0
  51. package/src/ngb/components/NgbDialog.vue +44 -0
  52. package/src/ngb/components/NgbDrawer.vue +156 -0
  53. package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
  54. package/src/ngb/components/NgbModalShell.vue +99 -0
  55. package/src/ngb/components/NgbToolbar.vue +14 -0
  56. package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
  57. package/src/ngb/components/forms/NgbFormRow.vue +21 -0
  58. package/src/ngb/components/forms/NgbFormSection.vue +16 -0
  59. package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
  60. package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
  61. package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
  62. package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
  63. package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
  64. package/src/ngb/components/register/registerTypes.ts +51 -0
  65. package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
  66. package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
  67. package/src/ngb/components/register/useRegisterRows.ts +209 -0
  68. package/src/ngb/components/register/useRegisterViewport.ts +56 -0
  69. package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
  70. package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
  71. package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
  72. package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
  73. package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
  74. package/src/ngb/editor/NgbEntityEditor.vue +250 -0
  75. package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
  76. package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
  77. package/src/ngb/editor/catalogNavigation.ts +13 -0
  78. package/src/ngb/editor/config.ts +252 -0
  79. package/src/ngb/editor/documentCopyDraft.ts +245 -0
  80. package/src/ngb/editor/documentEffects.ts +43 -0
  81. package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
  82. package/src/ngb/editor/documentNavigation.ts +115 -0
  83. package/src/ngb/editor/documentStatus.ts +45 -0
  84. package/src/ngb/editor/entityEditorErrors.ts +162 -0
  85. package/src/ngb/editor/entityEditorPersistence.ts +241 -0
  86. package/src/ngb/editor/extensions.ts +40 -0
  87. package/src/ngb/editor/types.ts +295 -0
  88. package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
  89. package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
  90. package/src/ngb/editor/useEditorDrawerState.ts +88 -0
  91. package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
  92. package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
  93. package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
  94. package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
  95. package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
  96. package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
  97. package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
  98. package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
  99. package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
  100. package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
  101. package/src/ngb/env/runtimeConfig.ts +25 -0
  102. package/src/ngb/lookup/config.ts +42 -0
  103. package/src/ngb/lookup/defaultConfig.ts +136 -0
  104. package/src/ngb/lookup/navigation.ts +76 -0
  105. package/src/ngb/lookup/prefetch.ts +40 -0
  106. package/src/ngb/lookup/store.ts +261 -0
  107. package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
  108. package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
  109. package/src/ngb/metadata/NgbEntityForm.vue +84 -0
  110. package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
  111. package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
  112. package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
  113. package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
  114. package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
  115. package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
  116. package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
  117. package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
  118. package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
  119. package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
  120. package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
  121. package/src/ngb/metadata/config.ts +28 -0
  122. package/src/ngb/metadata/dataTypes.ts +27 -0
  123. package/src/ngb/metadata/entityForm.ts +114 -0
  124. package/src/ngb/metadata/entityModel.ts +34 -0
  125. package/src/ngb/metadata/entityValueFormatting.ts +83 -0
  126. package/src/ngb/metadata/fieldRendererState.ts +84 -0
  127. package/src/ngb/metadata/filtering.ts +153 -0
  128. package/src/ngb/metadata/lookup.ts +25 -0
  129. package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
  130. package/src/ngb/metadata/normalization.ts +100 -0
  131. package/src/ngb/metadata/referenceHydration.ts +99 -0
  132. package/src/ngb/metadata/register.ts +135 -0
  133. package/src/ngb/metadata/routePages.ts +86 -0
  134. package/src/ngb/metadata/store.ts +35 -0
  135. package/src/ngb/metadata/types.ts +276 -0
  136. package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
  137. package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
  138. package/src/ngb/metadata/useValidationFocus.ts +49 -0
  139. package/src/ngb/primitives/NgbBadge.vue +31 -0
  140. package/src/ngb/primitives/NgbButton.vue +53 -0
  141. package/src/ngb/primitives/NgbCheckbox.vue +33 -0
  142. package/src/ngb/primitives/NgbDatePicker.vue +141 -0
  143. package/src/ngb/primitives/NgbIcon.vue +545 -0
  144. package/src/ngb/primitives/NgbInput.vue +57 -0
  145. package/src/ngb/primitives/NgbLookup.vue +264 -0
  146. package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
  147. package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
  148. package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
  149. package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
  150. package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
  151. package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
  152. package/src/ngb/primitives/NgbSelect.vue +108 -0
  153. package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
  154. package/src/ngb/primitives/NgbSwitch.vue +42 -0
  155. package/src/ngb/primitives/NgbTabs.vue +116 -0
  156. package/src/ngb/primitives/NgbToastHost.vue +49 -0
  157. package/src/ngb/primitives/NgbTooltip.vue +27 -0
  158. package/src/ngb/primitives/iconNames.ts +83 -0
  159. package/src/ngb/primitives/toast.ts +66 -0
  160. package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
  161. package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
  162. package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
  163. package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
  164. package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
  165. package/src/ngb/reporting/NgbReportPage.vue +1124 -0
  166. package/src/ngb/reporting/NgbReportSheet.vue +467 -0
  167. package/src/ngb/reporting/api.ts +35 -0
  168. package/src/ngb/reporting/composer.ts +904 -0
  169. package/src/ngb/reporting/config.ts +120 -0
  170. package/src/ngb/reporting/defaultConfig.ts +15 -0
  171. package/src/ngb/reporting/lookupFilters.ts +43 -0
  172. package/src/ngb/reporting/navigation.ts +181 -0
  173. package/src/ngb/reporting/pageHelpers.ts +73 -0
  174. package/src/ngb/reporting/pageSession.ts +92 -0
  175. package/src/ngb/reporting/paging.ts +66 -0
  176. package/src/ngb/reporting/types.ts +333 -0
  177. package/src/ngb/router/backNavigation.ts +122 -0
  178. package/src/ngb/router/queryParams.ts +129 -0
  179. package/src/ngb/router/queryState.ts +121 -0
  180. package/src/ngb/router/routeAliases.ts +66 -0
  181. package/src/ngb/router/routeParams.ts +15 -0
  182. package/src/ngb/router/shareLink.ts +39 -0
  183. package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
  184. package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
  185. package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
  186. package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
  187. package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
  188. package/src/ngb/security/NgbRolesPage.vue +125 -0
  189. package/src/ngb/security/NgbUserEditorPage.vue +615 -0
  190. package/src/ngb/security/NgbUsersPage.vue +125 -0
  191. package/src/ngb/security/api.ts +91 -0
  192. package/src/ngb/security/audit.ts +21 -0
  193. package/src/ngb/security/permissions.ts +65 -0
  194. package/src/ngb/security/types.ts +157 -0
  195. package/src/ngb/security/useAccessStore.ts +69 -0
  196. package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
  197. package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
  198. package/src/ngb/site/NgbLogo.vue +44 -0
  199. package/src/ngb/site/NgbPageHeader.vue +49 -0
  200. package/src/ngb/site/NgbSiteShell.vue +286 -0
  201. package/src/ngb/site/NgbSiteSidebar.vue +270 -0
  202. package/src/ngb/site/NgbTopBar.vue +425 -0
  203. package/src/ngb/site/NgbTrendChart.vue +224 -0
  204. package/src/ngb/site/dashboardData.ts +324 -0
  205. package/src/ngb/site/mainMenuStore.ts +49 -0
  206. package/src/ngb/site/types.ts +31 -0
  207. package/src/ngb/site/useDashboardPageState.ts +98 -0
  208. package/src/ngb/site/useTheme.ts +95 -0
  209. package/src/ngb/utils/clone.ts +43 -0
  210. package/src/ngb/utils/dateValues.ts +107 -0
  211. package/src/ngb/utils/errorMessage.ts +14 -0
  212. package/src/ngb/utils/guid.ts +21 -0
  213. package/src/ngb/utils/stableValue.ts +21 -0
  214. package/src/ngb/utils/storage.ts +135 -0
  215. package/src/styles/tailwind.css +165 -0
  216. package/src/vite/publicAssets.ts +104 -0
  217. package/vite-public-assets.js +68 -0
@@ -0,0 +1,716 @@
1
+ import { computed, ref, watch } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import { isNgbIconName, type NgbIconName } from '../primitives/iconNames'
4
+ import { useMainMenuStore, type MainMenuGroup } from '../site/mainMenuStore'
5
+ import { getConfiguredNgbCommandPalette } from './config'
6
+ import { loadCommandPaletteRecent, saveCommandPaletteRecent } from './storage'
7
+ import { defaultSearchFields, groupOrder, parseCommandPaletteQuery, scoreSearchText } from './search'
8
+ import { normalizeNgbRouteAliasPath } from '../router/routeAliases'
9
+ import type {
10
+ CommandPaletteExecutionMode,
11
+ CommandPaletteExplicitContext,
12
+ CommandPaletteGroup,
13
+ CommandPaletteGroupCode,
14
+ CommandPaletteItem,
15
+ CommandPaletteItemSeed,
16
+ CommandPaletteRecentEntry,
17
+ CommandPaletteScope,
18
+ CommandPaletteSearchContextDto,
19
+ } from './types'
20
+
21
+ const REMOTE_DEBOUNCE_MS = 150
22
+ const REMOTE_LIMIT = 20
23
+ const MAX_RECENT = 8
24
+
25
+ export const useCommandPaletteStore = defineStore('commandPalette', () => {
26
+ const config = getConfiguredNgbCommandPalette()
27
+ const menuStore = useMainMenuStore()
28
+
29
+ const isOpen = ref(false)
30
+ const query = ref('')
31
+ const activeIndex = ref(0)
32
+ const currentRoute = ref('/')
33
+ const focusRequestKey = ref(0)
34
+ const recentEntries = ref<CommandPaletteRecentEntry[]>([])
35
+ const reportItems = ref<CommandPaletteItemSeed[]>([])
36
+ const reportsLoaded = ref(false)
37
+ const reportsLoading = ref(false)
38
+ const remoteGroups = ref<CommandPaletteGroup[]>([])
39
+ const remoteLoading = ref(false)
40
+ const remoteError = ref<string | null>(null)
41
+ const explicitContextOwnerId = ref<string | null>(null)
42
+ const explicitContext = ref<CommandPaletteExplicitContext | null>(null)
43
+ const isHydrated = ref(false)
44
+
45
+ let remoteTimer: ReturnType<typeof setTimeout> | null = null
46
+ let remoteAbortController: AbortController | null = null
47
+ let remoteSequence = 0
48
+
49
+ const parsedQuery = computed(() => parseCommandPaletteQuery(query.value))
50
+ const cleanQuery = computed(() => parsedQuery.value.query)
51
+ const activeScope = computed<CommandPaletteScope | null>(() => parsedQuery.value.scope)
52
+
53
+ const localGroups = computed<CommandPaletteGroup[]>(() => {
54
+ const results: CommandPaletteGroup[] = []
55
+ const actions = limitGroupItems(
56
+ 'actions',
57
+ dedupeItemsByKey([
58
+ ...scoreItems(buildCurrentActionItems(), cleanQuery.value, activeScope.value),
59
+ ...scoreItems(buildFavoriteItems(), cleanQuery.value, activeScope.value),
60
+ ...scoreItems(buildCreateCommandItems(), cleanQuery.value, activeScope.value),
61
+ ]),
62
+ cleanQuery.value,
63
+ )
64
+
65
+ const goTo = limitGroupItems(
66
+ 'go-to',
67
+ scoreItems(buildGoToItems(menuStore.groups), cleanQuery.value, activeScope.value),
68
+ cleanQuery.value,
69
+ )
70
+
71
+ const reports = limitGroupItems(
72
+ 'reports',
73
+ scoreItems(buildReportItems(), cleanQuery.value, activeScope.value),
74
+ cleanQuery.value,
75
+ )
76
+
77
+ const recent = limitGroupItems(
78
+ 'recent',
79
+ scoreItems(buildRecentItems(), cleanQuery.value, activeScope.value),
80
+ cleanQuery.value,
81
+ )
82
+
83
+ if (actions.length > 0) results.push({ code: 'actions', label: 'Actions', items: actions })
84
+ if (goTo.length > 0) results.push({ code: 'go-to', label: 'Go to', items: goTo })
85
+ if (reports.length > 0) results.push({ code: 'reports', label: 'Reports', items: reports })
86
+ if (recent.length > 0) results.push({ code: 'recent', label: 'Recent', items: recent })
87
+
88
+ return results
89
+ })
90
+
91
+ const groups = computed<CommandPaletteGroup[]>(() => {
92
+ const merged = new Map<CommandPaletteGroupCode, CommandPaletteItem[]>()
93
+
94
+ for (const group of [...localGroups.value, ...remoteGroups.value]) {
95
+ const existing = merged.get(group.code) ?? []
96
+ const next = dedupeItemsByKey([...existing, ...group.items])
97
+ .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
98
+ merged.set(group.code, next)
99
+ }
100
+
101
+ return Array.from(merged.entries())
102
+ .sort((a, b) => groupOrder(a[0]) - groupOrder(b[0]))
103
+ .map(([code, items]) => ({
104
+ code,
105
+ label: resolveGroupLabel(code),
106
+ items: items.slice(0, visibleLimitForGroup(code, cleanQuery.value)),
107
+ }))
108
+ .filter((group) => group.items.length > 0)
109
+ })
110
+
111
+ const flatItems = computed(() => groups.value.flatMap((group) => group.items))
112
+
113
+ const hasResults = computed(() => flatItems.value.length > 0)
114
+ const hasRemoteError = computed(() => !!remoteError.value)
115
+ const showRemoteLoading = computed(() =>
116
+ remoteLoading.value
117
+ && cleanQuery.value.length >= 2
118
+ && activeScope.value !== 'commands'
119
+ && activeScope.value !== 'pages')
120
+
121
+ watch(flatItems, (items) => {
122
+ if (items.length === 0) {
123
+ activeIndex.value = 0
124
+ return
125
+ }
126
+
127
+ activeIndex.value = Math.max(0, Math.min(activeIndex.value, items.length - 1))
128
+ })
129
+
130
+ watch(
131
+ () => [
132
+ isOpen.value,
133
+ cleanQuery.value,
134
+ activeScope.value,
135
+ currentRoute.value,
136
+ explicitContext.value?.documentType,
137
+ explicitContext.value?.catalogType,
138
+ explicitContext.value?.entityId,
139
+ ] as const,
140
+ () => {
141
+ scheduleRemoteSearch()
142
+ },
143
+ )
144
+
145
+ async function hydrate(): Promise<void> {
146
+ if (isHydrated.value) return
147
+
148
+ recentEntries.value = loadCommandPaletteRecent(config.recentStorageKey)
149
+ currentRoute.value = config.router.currentRoute.value.fullPath || '/'
150
+ isHydrated.value = true
151
+ }
152
+
153
+ async function ensureReportItems(): Promise<void> {
154
+ if (reportsLoaded.value || reportsLoading.value || !config.loadReportItems) return
155
+
156
+ reportsLoading.value = true
157
+ try {
158
+ reportItems.value = await config.loadReportItems()
159
+ reportsLoaded.value = true
160
+ } catch (error) {
161
+ // eslint-disable-next-line no-console
162
+ console.error(error)
163
+ } finally {
164
+ reportsLoading.value = false
165
+ }
166
+ }
167
+
168
+ function open(): void {
169
+ void hydrate()
170
+ void ensureReportItems()
171
+ currentRoute.value = config.router.currentRoute.value.fullPath || currentRoute.value
172
+ isOpen.value = true
173
+ focusRequestKey.value += 1
174
+ if (!query.value) activeIndex.value = 0
175
+ }
176
+
177
+ function close(): void {
178
+ isOpen.value = false
179
+ query.value = ''
180
+ activeIndex.value = 0
181
+ remoteError.value = null
182
+ remoteLoading.value = false
183
+ remoteGroups.value = []
184
+ clearScheduledRemoteSearch()
185
+ }
186
+
187
+ function setQuery(nextValue: string): void {
188
+ query.value = nextValue
189
+ activeIndex.value = 0
190
+ }
191
+
192
+ function setCurrentRoute(route: string): void {
193
+ currentRoute.value = route || '/'
194
+ }
195
+
196
+ function setExplicitContext(ownerId: string, context: CommandPaletteExplicitContext | null): void {
197
+ explicitContextOwnerId.value = ownerId
198
+ explicitContext.value = context
199
+ }
200
+
201
+ function clearExplicitContext(ownerId: string): void {
202
+ if (explicitContextOwnerId.value !== ownerId) return
203
+ explicitContextOwnerId.value = null
204
+ explicitContext.value = null
205
+ }
206
+
207
+ function moveActive(delta: number): void {
208
+ if (flatItems.value.length === 0) return
209
+ activeIndex.value = Math.max(0, Math.min(activeIndex.value + delta, flatItems.value.length - 1))
210
+ }
211
+
212
+ function setActiveIndex(index: number): void {
213
+ if (flatItems.value.length === 0) {
214
+ activeIndex.value = 0
215
+ return
216
+ }
217
+
218
+ activeIndex.value = Math.max(0, Math.min(index, flatItems.value.length - 1))
219
+ }
220
+
221
+ async function executeActive(mode: CommandPaletteExecutionMode = 'default'): Promise<void> {
222
+ const item = flatItems.value[activeIndex.value]
223
+ if (!item) return
224
+ await executeItem(item, mode)
225
+ }
226
+
227
+ async function executeItem(item: CommandPaletteItem, mode: CommandPaletteExecutionMode = 'default'): Promise<void> {
228
+ close()
229
+
230
+ try {
231
+ if (mode === 'new-tab' && item.route && item.openInNewTabSupported) {
232
+ openRouteInNewTab(item.route)
233
+ recordRecent(item)
234
+ return
235
+ }
236
+
237
+ if (item.perform) {
238
+ await item.perform()
239
+ } else if (item.route) {
240
+ await config.router.push(item.route)
241
+ }
242
+
243
+ recordRecent(item)
244
+ } catch (error) {
245
+ // eslint-disable-next-line no-console
246
+ console.error(error)
247
+ }
248
+ }
249
+
250
+ function recordRecent(item: CommandPaletteItem): void {
251
+ if (!isRecentTrackable(item)) return
252
+
253
+ const nextEntry: CommandPaletteRecentEntry = {
254
+ key: item.key,
255
+ kind: item.kind,
256
+ scope: item.scope,
257
+ title: item.title,
258
+ subtitle: item.subtitle ?? null,
259
+ icon: item.icon ?? null,
260
+ badge: item.badge ?? null,
261
+ route: item.route ?? null,
262
+ status: item.status ?? null,
263
+ openInNewTabSupported: Boolean(item.openInNewTabSupported),
264
+ timestamp: new Date().toISOString(),
265
+ }
266
+
267
+ recentEntries.value = [nextEntry, ...recentEntries.value.filter((entry) => entry.key !== nextEntry.key)].slice(0, MAX_RECENT)
268
+ saveCommandPaletteRecent(config.recentStorageKey, recentEntries.value)
269
+ }
270
+
271
+ function clearScheduledRemoteSearch(): void {
272
+ if (remoteTimer) {
273
+ clearTimeout(remoteTimer)
274
+ remoteTimer = null
275
+ }
276
+
277
+ remoteAbortController?.abort()
278
+ remoteAbortController = null
279
+ }
280
+
281
+ function scheduleRemoteSearch(): void {
282
+ clearScheduledRemoteSearch()
283
+
284
+ if (!isOpen.value || !shouldRunRemoteSearch(cleanQuery.value, activeScope.value, Boolean(config.searchRemote))) {
285
+ remoteLoading.value = false
286
+ remoteError.value = null
287
+ remoteGroups.value = []
288
+ return
289
+ }
290
+
291
+ remoteLoading.value = true
292
+ remoteError.value = null
293
+ const seq = ++remoteSequence
294
+
295
+ remoteTimer = setTimeout(async () => {
296
+ remoteAbortController = new AbortController()
297
+ try {
298
+ const response = await config.searchRemote!({
299
+ query: cleanQuery.value,
300
+ scope: activeScope.value ?? null,
301
+ limit: REMOTE_LIMIT,
302
+ currentRoute: currentRoute.value,
303
+ context: buildSearchContext(explicitContext.value),
304
+ }, remoteAbortController.signal)
305
+
306
+ if (seq !== remoteSequence) return
307
+
308
+ remoteGroups.value = response.groups
309
+ .filter((group) => group.code !== 'reports')
310
+ .map((group) => ({
311
+ code: group.code as CommandPaletteGroupCode,
312
+ label: group.label,
313
+ items: group.items.map((item) => ({
314
+ key: item.key,
315
+ group: group.code as CommandPaletteGroupCode,
316
+ kind: item.kind as CommandPaletteItem['kind'],
317
+ scope: scopeForRemoteKind(item.kind),
318
+ title: item.title,
319
+ subtitle: item.subtitle ?? null,
320
+ icon: resolveIconName(item.icon, item.kind === 'catalog' ? 'grid' : 'file-text'),
321
+ badge: item.badge ?? null,
322
+ hint: null,
323
+ route: item.route ?? null,
324
+ commandCode: item.commandCode ?? null,
325
+ status: item.status ?? null,
326
+ openInNewTabSupported: item.openInNewTabSupported,
327
+ keywords: [],
328
+ defaultRank: 0,
329
+ score: Number(item.score ?? 0),
330
+ source: 'remote',
331
+ })),
332
+ }))
333
+ } catch (error) {
334
+ if (remoteAbortController?.signal.aborted) return
335
+ if (seq !== remoteSequence) return
336
+
337
+ remoteGroups.value = []
338
+ remoteError.value = error instanceof Error && error.message.trim()
339
+ ? error.message
340
+ : 'Could not update remote results.'
341
+ } finally {
342
+ if (seq === remoteSequence) remoteLoading.value = false
343
+ }
344
+ }, REMOTE_DEBOUNCE_MS)
345
+ }
346
+
347
+ function buildCurrentActionItems(): CommandPaletteItem[] {
348
+ const explicit = explicitContext.value?.actions ?? []
349
+ const heuristic = config.buildHeuristicCurrentActions?.(currentRoute.value) ?? []
350
+ return materializeLocalItems(dedupeSeedsByKey([...explicit, ...heuristic]), {
351
+ source: 'local',
352
+ score: 0,
353
+ isCurrentContext: true,
354
+ iconFallback: 'search',
355
+ rankFrom: 1_000,
356
+ })
357
+ }
358
+
359
+ function buildFavoriteItems(): CommandPaletteItem[] {
360
+ return materializeLocalItems((config.favoriteItems ?? []).filter(isStaticNavigationItemAllowed), {
361
+ source: 'local',
362
+ score: 0,
363
+ isFavorite: true,
364
+ rankFrom: 820,
365
+ iconFallback: 'file-text',
366
+ })
367
+ }
368
+
369
+ function buildCreateCommandItems(): CommandPaletteItem[] {
370
+ return materializeLocalItems((config.createItems ?? []).filter(isStaticNavigationItemAllowed), {
371
+ source: 'local',
372
+ score: 0,
373
+ rankFrom: 780,
374
+ iconFallback: 'plus',
375
+ })
376
+ }
377
+
378
+ function buildReportItems(): CommandPaletteItem[] {
379
+ return materializeLocalItems(reportItems.value, {
380
+ source: 'local',
381
+ score: 0,
382
+ rankFrom: 700,
383
+ iconFallback: 'bar-chart',
384
+ })
385
+ }
386
+
387
+ function buildGoToItems(groups: MainMenuGroup[]): CommandPaletteItem[] {
388
+ const allowedNormalizedRoutes = new Set(
389
+ groups.flatMap((group) => group.items.map((item) => normalizeRouteForPermissionFiltering(item.route))),
390
+ )
391
+
392
+ const items = groups
393
+ .flatMap((group, groupIndex) => group.items.map((item, itemIndex) => ({ group, item, groupIndex, itemIndex })))
394
+ .filter(({ item }) => !String(item.route ?? '').startsWith('/reports/'))
395
+ .map(({ group, item, groupIndex, itemIndex }) => ({
396
+ key: `page:${item.code}`,
397
+ group: 'go-to' as const,
398
+ kind: 'page' as const,
399
+ scope: 'pages' as const,
400
+ title: item.label,
401
+ subtitle: group.label,
402
+ icon: resolveIconName(item.icon, item.route.startsWith('/catalogs/') ? 'grid' : 'file-text'),
403
+ badge: 'Page',
404
+ hint: null,
405
+ route: item.route,
406
+ commandCode: null,
407
+ status: null,
408
+ openInNewTabSupported: true,
409
+ keywords: [item.code, group.label],
410
+ defaultRank: 650 - (groupIndex * 20) - itemIndex,
411
+ score: 0,
412
+ source: 'local' as const,
413
+ }))
414
+
415
+ const existingRoutes = new Set(items.map((item) => normalizeRouteForPermissionFiltering(item.route)))
416
+ const fallbackPages = materializeLocalItems(
417
+ (config.specialPageItems ?? []).filter((item) => {
418
+ const route = normalizeRouteForPermissionFiltering(item.route)
419
+ return !!route && allowedNormalizedRoutes.has(route) && !existingRoutes.has(route)
420
+ }),
421
+ {
422
+ source: 'local',
423
+ score: 0,
424
+ rankFrom: 560,
425
+ iconFallback: 'file-text',
426
+ },
427
+ )
428
+
429
+ return [...items, ...fallbackPages]
430
+ }
431
+
432
+ function buildRecentItems(): CommandPaletteItem[] {
433
+ return recentEntries.value
434
+ .filter((entry) => isRouteAllowedByCurrentAccess(entry.route))
435
+ .map((entry, index) => ({
436
+ key: `recent:${entry.key}`,
437
+ group: 'recent',
438
+ kind: 'recent',
439
+ scope: entry.scope,
440
+ title: entry.title,
441
+ subtitle: entry.subtitle ?? null,
442
+ icon: resolveIconName(entry.icon, entry.scope === 'reports' ? 'bar-chart' : 'file-text'),
443
+ badge: entry.badge ?? null,
444
+ hint: null,
445
+ route: entry.route ?? null,
446
+ commandCode: null,
447
+ status: entry.status ?? null,
448
+ openInNewTabSupported: entry.openInNewTabSupported,
449
+ keywords: [],
450
+ defaultRank: 500 - index,
451
+ score: 0,
452
+ isRecent: true,
453
+ source: 'local',
454
+ }))
455
+ }
456
+
457
+ function isStaticNavigationItemAllowed(item: CommandPaletteItemSeed): boolean {
458
+ if (!item.route) return true
459
+ return isRouteAllowedByCurrentAccess(item.route)
460
+ }
461
+
462
+ function isRouteAllowedByCurrentAccess(route: string | null | undefined): boolean {
463
+ const candidate = normalizeRouteForPermissionFiltering(route)
464
+ if (!candidate) return false
465
+
466
+ const allowedRoutes = [
467
+ ...menuStore.groups.flatMap((group) => group.items.map((item) => item.route)),
468
+ ...reportItems.value.map((item) => item.route),
469
+ ]
470
+
471
+ return allowedRoutes.some((allowedRoute) => {
472
+ const allowed = normalizeRouteForPermissionFiltering(allowedRoute)
473
+ return !!allowed && (candidate === allowed || candidate.startsWith(`${allowed}/`))
474
+ })
475
+ }
476
+
477
+ return {
478
+ isOpen,
479
+ query,
480
+ activeIndex,
481
+ focusRequestKey,
482
+ groups,
483
+ flatItems,
484
+ activeScope,
485
+ cleanQuery,
486
+ remoteError,
487
+ hasRemoteError,
488
+ remoteLoading,
489
+ showRemoteLoading,
490
+ hasResults,
491
+ hydrate,
492
+ open,
493
+ close,
494
+ setQuery,
495
+ setCurrentRoute,
496
+ setExplicitContext,
497
+ clearExplicitContext,
498
+ moveActive,
499
+ setActiveIndex,
500
+ executeActive,
501
+ executeItem,
502
+ }
503
+ })
504
+
505
+ function materializeLocalItems(
506
+ items: CommandPaletteItemSeed[],
507
+ options: {
508
+ source: CommandPaletteItem['source']
509
+ score: number
510
+ rankFrom: number
511
+ iconFallback: NgbIconName
512
+ isCurrentContext?: boolean
513
+ isFavorite?: boolean
514
+ isRecent?: boolean
515
+ },
516
+ ): CommandPaletteItem[] {
517
+ return items.map((item, index) => ({
518
+ ...item,
519
+ icon: resolveIconName(item.icon, item.scope === 'reports' ? 'bar-chart' : options.iconFallback),
520
+ defaultRank: item.defaultRank || options.rankFrom - index,
521
+ source: options.source,
522
+ score: options.score,
523
+ isCurrentContext: item.isCurrentContext ?? options.isCurrentContext,
524
+ isFavorite: item.isFavorite ?? options.isFavorite,
525
+ isRecent: item.isRecent ?? options.isRecent,
526
+ }))
527
+ }
528
+
529
+ function dedupeSeedsByKey(items: CommandPaletteItemSeed[]): CommandPaletteItemSeed[] {
530
+ const seen = new Set<string>()
531
+ const result: CommandPaletteItemSeed[] = []
532
+
533
+ for (const item of items) {
534
+ if (!item || seen.has(item.key)) continue
535
+ seen.add(item.key)
536
+ result.push(item)
537
+ }
538
+
539
+ return result
540
+ }
541
+
542
+ function resolveGroupLabel(code: CommandPaletteGroupCode): string {
543
+ switch (code) {
544
+ case 'actions':
545
+ return 'Actions'
546
+ case 'go-to':
547
+ return 'Go to'
548
+ case 'documents':
549
+ return 'Documents'
550
+ case 'catalogs':
551
+ return 'Catalogs'
552
+ case 'reports':
553
+ return 'Reports'
554
+ case 'recent':
555
+ return 'Recent'
556
+ }
557
+ }
558
+
559
+ function visibleLimitForGroup(code: CommandPaletteGroupCode, query: string): number {
560
+ if (query.trim().length > 0) return 8
561
+
562
+ switch (code) {
563
+ case 'actions':
564
+ return 10
565
+ case 'go-to':
566
+ return 6
567
+ case 'reports':
568
+ return 6
569
+ case 'recent':
570
+ return 5
571
+ default:
572
+ return 6
573
+ }
574
+ }
575
+
576
+ function limitGroupItems(code: CommandPaletteGroupCode, items: CommandPaletteItem[], query: string): CommandPaletteItem[] {
577
+ return items
578
+ .sort((a, b) => b.score - a.score || b.defaultRank - a.defaultRank || a.title.localeCompare(b.title))
579
+ .slice(0, visibleLimitForGroup(code, query))
580
+ }
581
+
582
+ function normalizeRouteForPermissionFiltering(route: string | null | undefined): string {
583
+ return normalizeNgbRouteAliasPath(route).split(/[?#]/, 1)[0] ?? ''
584
+ }
585
+
586
+ function scoreItems(items: CommandPaletteItem[], query: string, scope: CommandPaletteScope | null): CommandPaletteItem[] {
587
+ const textQuery = query.trim()
588
+
589
+ return items
590
+ .filter((item) => item.scope === scope || scope == null)
591
+ .map((item) => {
592
+ if (!textQuery) {
593
+ return {
594
+ ...item,
595
+ score: item.defaultRank,
596
+ }
597
+ }
598
+
599
+ const score = scoreSearchText(textQuery, [
600
+ ...defaultSearchFields(item.title, item.subtitle ?? '', item.badge ?? ''),
601
+ ...defaultSearchFields(...(item.keywords ?? [])),
602
+ ])
603
+
604
+ if (score <= 0) return null
605
+
606
+ const boostedScore = score
607
+ + (item.isCurrentContext ? 0.08 : 0)
608
+ + (item.isFavorite ? 0.04 : 0)
609
+ + (item.isRecent ? 0.02 : 0)
610
+ + (item.scope === 'commands' ? 0.01 : 0)
611
+
612
+ return {
613
+ ...item,
614
+ score: boostedScore,
615
+ }
616
+ })
617
+ .filter((item): item is CommandPaletteItem => !!item)
618
+ }
619
+
620
+ function dedupeItemsByKey(items: CommandPaletteItem[]): CommandPaletteItem[] {
621
+ const seen = new Set<string>()
622
+ const result: CommandPaletteItem[] = []
623
+
624
+ for (const item of items) {
625
+ if (!item || seen.has(item.key)) continue
626
+ seen.add(item.key)
627
+ result.push(item)
628
+ }
629
+
630
+ return result
631
+ }
632
+
633
+ function shouldRunRemoteSearch(query: string, scope: CommandPaletteScope | null, remoteEnabled: boolean): boolean {
634
+ if (!remoteEnabled) return false
635
+ if (query.trim().length < 2) return false
636
+ if (scope === 'commands' || scope === 'pages' || scope === 'reports') return false
637
+ return true
638
+ }
639
+
640
+ function buildSearchContext(context: CommandPaletteExplicitContext | null): CommandPaletteSearchContextDto | null {
641
+ if (!context) return null
642
+
643
+ return {
644
+ entityType: context.entityType ?? null,
645
+ documentType: context.documentType ?? null,
646
+ catalogType: context.catalogType ?? null,
647
+ entityId: looksLikeGuid(context.entityId) ? context.entityId : null,
648
+ }
649
+ }
650
+
651
+ function looksLikeGuid(value: string | null | undefined): value is string {
652
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(value ?? '').trim())
653
+ }
654
+
655
+ function scopeForRemoteKind(kind: string): CommandPaletteScope {
656
+ switch (kind) {
657
+ case 'document':
658
+ return 'documents'
659
+ case 'catalog':
660
+ return 'catalogs'
661
+ case 'report':
662
+ return 'reports'
663
+ default:
664
+ return 'pages'
665
+ }
666
+ }
667
+
668
+ function isRecentTrackable(item: CommandPaletteItem): boolean {
669
+ return Boolean(item.route) && (item.kind === 'page' || item.kind === 'document' || item.kind === 'catalog' || item.kind === 'report' || item.kind === 'recent')
670
+ }
671
+
672
+ function openRouteInNewTab(route: string): void {
673
+ const { router } = getConfiguredNgbCommandPalette()
674
+ const href = router.resolve(route).href
675
+ if (typeof window === 'undefined') return
676
+ window.open(href, '_blank', 'noopener,noreferrer')
677
+ }
678
+
679
+ function resolveIconName(icon: string | null | undefined, fallback: NgbIconName): NgbIconName {
680
+ const value = String(icon ?? '').trim()
681
+ if (!value) return fallback
682
+ if (isNgbIconName(value)) return value
683
+
684
+ switch (value) {
685
+ case 'list':
686
+ case 'receipt':
687
+ case 'book-open':
688
+ case 'calculator':
689
+ case 'users':
690
+ case 'building-2':
691
+ case 'coins':
692
+ case 'wallet':
693
+ case 'wrench':
694
+ case 'clipboard-list':
695
+ case 'check-square':
696
+ case 'tag':
697
+ case 'landmark':
698
+ case 'calendar-check':
699
+ case 'scale':
700
+ case 'git-merge':
701
+ case 'shield-check':
702
+ case 'heart-pulse':
703
+ case 'cogs':
704
+ return value
705
+ case 'chart':
706
+ return 'bar-chart'
707
+ case 'file':
708
+ return 'file-text'
709
+ case 'book':
710
+ return 'book-open'
711
+ case 'folder':
712
+ return 'grid'
713
+ default:
714
+ return fallback
715
+ }
716
+ }