@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.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<TransitionRoot appear :show="store.isOpen" as="template" @after-leave="restoreLastFocus">
|
|
4
|
+
<Dialog as="div" class="relative z-[90]" :initialFocus="inputRef" @close="store.close()">
|
|
5
|
+
<TransitionChild
|
|
6
|
+
as="template"
|
|
7
|
+
enter="duration-150 ease-out"
|
|
8
|
+
enter-from="opacity-0"
|
|
9
|
+
enter-to="opacity-100"
|
|
10
|
+
leave="duration-120 ease-in"
|
|
11
|
+
leave-from="opacity-100"
|
|
12
|
+
leave-to="opacity-0"
|
|
13
|
+
>
|
|
14
|
+
<div class="fixed inset-0 bg-[rgba(7,12,20,.48)] backdrop-blur-[2px]" />
|
|
15
|
+
</TransitionChild>
|
|
16
|
+
|
|
17
|
+
<div class="fixed inset-0 overflow-y-auto">
|
|
18
|
+
<div class="flex min-h-full items-start justify-center px-4 pb-8 pt-[10vh]">
|
|
19
|
+
<TransitionChild
|
|
20
|
+
as="template"
|
|
21
|
+
enter="duration-150 ease-out"
|
|
22
|
+
enter-from="opacity-0 translate-y-3 scale-[0.985]"
|
|
23
|
+
enter-to="opacity-100 translate-y-0 scale-100"
|
|
24
|
+
leave="duration-120 ease-in"
|
|
25
|
+
leave-from="opacity-100 translate-y-0 scale-100"
|
|
26
|
+
leave-to="opacity-0 translate-y-3 scale-[0.985]"
|
|
27
|
+
>
|
|
28
|
+
<DialogPanel data-testid="command-palette-dialog" class="w-full max-w-[860px] overflow-hidden rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card">
|
|
29
|
+
<DialogTitle class="sr-only">Command palette</DialogTitle>
|
|
30
|
+
|
|
31
|
+
<div class="border-b border-ngb-border px-4 py-3">
|
|
32
|
+
<div class="flex h-10 items-center gap-3 rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3">
|
|
33
|
+
<span class="text-ngb-muted">
|
|
34
|
+
<NgbIcon name="search" :size="18" />
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<input
|
|
38
|
+
ref="inputRef"
|
|
39
|
+
data-testid="command-palette-input"
|
|
40
|
+
:value="store.query"
|
|
41
|
+
class="h-full flex-1 border-0 bg-transparent px-0 text-sm text-ngb-text outline-none placeholder:text-ngb-muted/70"
|
|
42
|
+
:placeholder="placeholder"
|
|
43
|
+
role="combobox"
|
|
44
|
+
aria-autocomplete="list"
|
|
45
|
+
:aria-expanded="store.hasResults"
|
|
46
|
+
:aria-controls="listboxId"
|
|
47
|
+
:aria-activedescendant="activeDescendantId"
|
|
48
|
+
@input="store.setQuery(String(($event.target as HTMLInputElement)?.value ?? ''))"
|
|
49
|
+
@keydown="onInputKeyDown"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<span class="hidden items-center gap-1 md:flex">
|
|
53
|
+
<template v-if="isMac">
|
|
54
|
+
<span class="ngb-kbd">⌘</span>
|
|
55
|
+
</template>
|
|
56
|
+
<template v-else>
|
|
57
|
+
<span class="ngb-kbd">Ctrl</span>
|
|
58
|
+
</template>
|
|
59
|
+
<span class="ngb-kbd">K</span>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div v-if="scopeBadgeLabel || store.showRemoteLoading" class="mt-2 flex flex-wrap items-center gap-2 text-xs text-ngb-muted">
|
|
64
|
+
<span v-if="scopeBadgeLabel" class="rounded-full border border-ngb-border bg-ngb-bg px-2.5 py-1">
|
|
65
|
+
{{ scopeBadgeLabel }}
|
|
66
|
+
</span>
|
|
67
|
+
<span v-if="store.showRemoteLoading" class="rounded-full border border-ngb-border bg-ngb-bg px-2.5 py-1">
|
|
68
|
+
Updating remote results…
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="sr-only" aria-live="polite">
|
|
74
|
+
{{ liveRegionText }}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="max-h-[560px] overflow-y-auto p-2" :id="listboxId" role="listbox">
|
|
78
|
+
<div
|
|
79
|
+
v-if="store.hasRemoteError && store.groups.length > 0"
|
|
80
|
+
class="mx-2 mb-2 rounded-[var(--ngb-radius)] border border-[rgba(185,28,28,.16)] bg-[rgba(254,242,242,.92)] px-3 py-2 text-xs text-[#7f1d1d]"
|
|
81
|
+
>
|
|
82
|
+
{{ store.remoteError }}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<template v-if="store.groups.length > 0">
|
|
86
|
+
<section v-for="group in store.groups" :key="group.code" class="py-1.5">
|
|
87
|
+
<div class="px-3 pb-2 pt-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-ngb-muted">
|
|
88
|
+
{{ group.label }}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="space-y-1">
|
|
92
|
+
<button
|
|
93
|
+
v-for="item in group.items"
|
|
94
|
+
:id="optionId(item.key)"
|
|
95
|
+
:key="item.key"
|
|
96
|
+
:data-option-id="optionId(item.key)"
|
|
97
|
+
:data-item-key="item.key"
|
|
98
|
+
type="button"
|
|
99
|
+
role="option"
|
|
100
|
+
:aria-selected="isActive(item.key)"
|
|
101
|
+
class="flex w-full items-start gap-3 rounded-[var(--ngb-radius)] border px-3 py-2.5 text-left transition-colors"
|
|
102
|
+
:class="isActive(item.key)
|
|
103
|
+
? 'border-[rgba(11,60,93,.22)] bg-[rgba(11,60,93,.08)]'
|
|
104
|
+
: 'border-transparent hover:border-ngb-border hover:bg-ngb-bg'"
|
|
105
|
+
@mouseenter="setActive(item.key)"
|
|
106
|
+
@click="onItemClick(item, $event)"
|
|
107
|
+
>
|
|
108
|
+
<span
|
|
109
|
+
class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--ngb-radius)]"
|
|
110
|
+
:class="isActive(item.key) ? 'bg-transparent text-ngb-text' : 'bg-transparent text-ngb-muted'"
|
|
111
|
+
>
|
|
112
|
+
<NgbIcon :name="resolveCommandPaletteIcon(item.icon)" :size="17" />
|
|
113
|
+
</span>
|
|
114
|
+
|
|
115
|
+
<span class="min-w-0 flex-1">
|
|
116
|
+
<span class="flex items-start justify-between gap-3">
|
|
117
|
+
<span class="min-w-0">
|
|
118
|
+
<span class="block truncate text-sm font-semibold text-ngb-text">{{ item.title }}</span>
|
|
119
|
+
<span class="mt-0.5 block truncate text-xs leading-5 text-ngb-muted">{{ resolveCommandPaletteSubtitle(item) }}</span>
|
|
120
|
+
</span>
|
|
121
|
+
|
|
122
|
+
<span class="flex shrink-0 items-center gap-2">
|
|
123
|
+
<NgbBadge
|
|
124
|
+
v-if="resolveCommandPaletteBadge(item)"
|
|
125
|
+
tone="neutral"
|
|
126
|
+
>
|
|
127
|
+
{{ resolveCommandPaletteBadge(item) }}
|
|
128
|
+
</NgbBadge>
|
|
129
|
+
<span v-if="item.openInNewTabSupported" class="hidden text-[11px] text-ngb-muted lg:inline">
|
|
130
|
+
{{ primaryModifier }}+Enter
|
|
131
|
+
</span>
|
|
132
|
+
</span>
|
|
133
|
+
</span>
|
|
134
|
+
</span>
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<NgbCommandPaletteEmptyState
|
|
141
|
+
v-else
|
|
142
|
+
:query="store.cleanQuery"
|
|
143
|
+
:loading="store.showRemoteLoading"
|
|
144
|
+
:error="store.hasRemoteError ? store.remoteError : null"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<NgbCommandPaletteFooterHints :primary-modifier="primaryModifier" />
|
|
149
|
+
</DialogPanel>
|
|
150
|
+
</TransitionChild>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</Dialog>
|
|
154
|
+
</TransitionRoot>
|
|
155
|
+
</Teleport>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<script setup lang="ts">
|
|
159
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
160
|
+
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
|
|
161
|
+
import NgbBadge from '../primitives/NgbBadge.vue'
|
|
162
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
163
|
+
import { useCommandPaletteStore } from './store'
|
|
164
|
+
import type { CommandPaletteItem } from './types'
|
|
165
|
+
import {
|
|
166
|
+
resolveCommandPaletteBadge,
|
|
167
|
+
resolveCommandPaletteIcon,
|
|
168
|
+
resolveCommandPaletteSubtitle,
|
|
169
|
+
} from './presentation'
|
|
170
|
+
import NgbCommandPaletteEmptyState from './NgbCommandPaletteEmptyState.vue'
|
|
171
|
+
import NgbCommandPaletteFooterHints from './NgbCommandPaletteFooterHints.vue'
|
|
172
|
+
|
|
173
|
+
const store = useCommandPaletteStore()
|
|
174
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
175
|
+
const lastFocusedElement = ref<HTMLElement | null>(null)
|
|
176
|
+
let restoreFocusTimer: ReturnType<typeof window.setTimeout> | null = null
|
|
177
|
+
|
|
178
|
+
const listboxId = 'ngb-command-palette-listbox'
|
|
179
|
+
const placeholder = 'Search pages, records, reports, or run a command…'
|
|
180
|
+
const isMac = /Mac|iPhone|iPad|iPod/i.test(String(globalThis.navigator?.platform ?? ''))
|
|
181
|
+
const primaryModifier = computed(() => (isMac ? '⌘' : 'Ctrl'))
|
|
182
|
+
|
|
183
|
+
function onDocumentFocusIn(event: FocusEvent): void {
|
|
184
|
+
if (store.isOpen) return
|
|
185
|
+
const target = event.target instanceof HTMLElement ? event.target : null
|
|
186
|
+
if (!target || target === document.body) return
|
|
187
|
+
lastFocusedElement.value = target
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const flatItems = computed(() => store.flatItems)
|
|
191
|
+
const indexByKey = computed(() => {
|
|
192
|
+
const map = new Map<string, number>()
|
|
193
|
+
flatItems.value.forEach((item, index) => map.set(item.key, index))
|
|
194
|
+
return map
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const activeItem = computed(() => flatItems.value[store.activeIndex] ?? null)
|
|
198
|
+
const activeDescendantId = computed(() => (activeItem.value ? optionId(activeItem.value.key) : undefined))
|
|
199
|
+
const scopeBadgeLabel = computed<string | null>(() => {
|
|
200
|
+
switch (store.activeScope) {
|
|
201
|
+
case 'commands':
|
|
202
|
+
return 'Commands'
|
|
203
|
+
case 'pages':
|
|
204
|
+
return 'Pages'
|
|
205
|
+
case 'reports':
|
|
206
|
+
return 'Reports'
|
|
207
|
+
case 'documents':
|
|
208
|
+
return 'Documents'
|
|
209
|
+
case 'catalogs':
|
|
210
|
+
return 'Catalogs'
|
|
211
|
+
default:
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const liveRegionText = computed(() => {
|
|
217
|
+
if (store.hasRemoteError) return store.remoteError ?? 'Remote search is unavailable.'
|
|
218
|
+
if (store.showRemoteLoading) return 'Updating remote results.'
|
|
219
|
+
if (store.groups.length === 0) return store.cleanQuery ? `No results for ${store.cleanQuery}.` : 'Showing command palette shortcuts.'
|
|
220
|
+
return `${flatItems.value.length} results available.`
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
watch(
|
|
224
|
+
() => store.isOpen,
|
|
225
|
+
async (open) => {
|
|
226
|
+
if (open) {
|
|
227
|
+
if (restoreFocusTimer) {
|
|
228
|
+
window.clearTimeout(restoreFocusTimer)
|
|
229
|
+
restoreFocusTimer = null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const activeElement = document.activeElement
|
|
233
|
+
if (activeElement instanceof HTMLElement && activeElement !== document.body)
|
|
234
|
+
lastFocusedElement.value = activeElement
|
|
235
|
+
await nextTick()
|
|
236
|
+
focusInput(true)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const restoreTarget = lastFocusedElement.value
|
|
241
|
+
restoreFocusTimer = window.setTimeout(() => {
|
|
242
|
+
restoreFocusTimer = null
|
|
243
|
+
if (store.isOpen) return
|
|
244
|
+
restoreTarget?.focus?.()
|
|
245
|
+
}, 180)
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
watch(
|
|
250
|
+
() => store.focusRequestKey,
|
|
251
|
+
async () => {
|
|
252
|
+
if (!store.isOpen) return
|
|
253
|
+
await nextTick()
|
|
254
|
+
focusInput(true)
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
watch(
|
|
259
|
+
() => activeDescendantId.value,
|
|
260
|
+
async (id) => {
|
|
261
|
+
if (!id) return
|
|
262
|
+
await nextTick()
|
|
263
|
+
document.querySelector<HTMLElement>(`[data-option-id="${id}"]`)?.scrollIntoView({ block: 'nearest' })
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
function focusInput(selectAll = false, attempt = 0): void {
|
|
268
|
+
inputRef.value?.focus()
|
|
269
|
+
if (selectAll) inputRef.value?.select()
|
|
270
|
+
window.setTimeout(() => {
|
|
271
|
+
if (!store.isOpen) return
|
|
272
|
+
if (document.activeElement === inputRef.value) return
|
|
273
|
+
if (attempt >= 4) return
|
|
274
|
+
focusInput(selectAll, attempt + 1)
|
|
275
|
+
}, attempt === 0 ? 0 : 16)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function restoreLastFocus(): void {
|
|
279
|
+
if (store.isOpen) return
|
|
280
|
+
if (restoreFocusTimer) {
|
|
281
|
+
window.clearTimeout(restoreFocusTimer)
|
|
282
|
+
restoreFocusTimer = null
|
|
283
|
+
}
|
|
284
|
+
window.setTimeout(() => {
|
|
285
|
+
if (store.isOpen) return
|
|
286
|
+
lastFocusedElement.value?.focus?.()
|
|
287
|
+
}, 0)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
onMounted(() => {
|
|
291
|
+
document.addEventListener('focusin', onDocumentFocusIn, true)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
onBeforeUnmount(() => {
|
|
295
|
+
document.removeEventListener('focusin', onDocumentFocusIn, true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
function isActive(key: string): boolean {
|
|
299
|
+
return activeItem.value?.key === key
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function setActive(key: string): void {
|
|
303
|
+
const index = indexByKey.value.get(key)
|
|
304
|
+
if (index == null) return
|
|
305
|
+
store.setActiveIndex(index)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function optionId(key: string): string {
|
|
309
|
+
return `ngb-command-palette-option-${key.replace(/[^a-z0-9_-]+/gi, '-')}`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function onInputKeyDown(event: KeyboardEvent): void {
|
|
313
|
+
if (event.key === 'ArrowDown') {
|
|
314
|
+
event.preventDefault()
|
|
315
|
+
store.moveActive(1)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (event.key === 'ArrowUp') {
|
|
320
|
+
event.preventDefault()
|
|
321
|
+
store.moveActive(-1)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (event.key === 'Escape') {
|
|
326
|
+
event.preventDefault()
|
|
327
|
+
event.stopPropagation()
|
|
328
|
+
store.close()
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (event.key === 'Enter') {
|
|
333
|
+
event.preventDefault()
|
|
334
|
+
void store.executeActive((event.metaKey || event.ctrlKey) ? 'new-tab' : 'default')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function onItemClick(item: CommandPaletteItem, event: MouseEvent): void {
|
|
339
|
+
const mode = (event.metaKey || event.ctrlKey) && item.openInNewTabSupported ? 'new-tab' : 'default'
|
|
340
|
+
void store.executeItem(item, mode)
|
|
341
|
+
}
|
|
342
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="px-5 py-8">
|
|
3
|
+
<template v-if="error">
|
|
4
|
+
<div
|
|
5
|
+
role="alert"
|
|
6
|
+
aria-live="assertive"
|
|
7
|
+
class="rounded-[var(--ngb-radius)] border border-[rgba(185,28,28,.18)] bg-[rgba(254,242,242,.9)] px-4 py-3"
|
|
8
|
+
>
|
|
9
|
+
<div class="text-sm font-semibold text-[#991b1b]">Remote search is temporarily unavailable</div>
|
|
10
|
+
<div class="mt-1 text-sm text-[#7f1d1d]">{{ error }}</div>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<template v-else-if="loading">
|
|
15
|
+
<div role="status" aria-live="polite">
|
|
16
|
+
<div class="text-sm font-semibold text-ngb-text">Searching records…</div>
|
|
17
|
+
<div class="mt-2 text-sm text-ngb-muted">Pages, commands, and cached reports stay available while remote results load.</div>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<template v-else-if="!query">
|
|
22
|
+
<div class="text-sm font-semibold text-ngb-text">Search pages, records, reports, or run a command</div>
|
|
23
|
+
<div class="mt-2 text-sm text-ngb-muted">
|
|
24
|
+
Try <span class="font-medium text-ngb-text">trial balance</span> or <span class="font-medium text-ngb-text">create journal entry</span>.
|
|
25
|
+
</div>
|
|
26
|
+
<div class="mt-4 flex flex-wrap gap-2 text-xs text-ngb-muted">
|
|
27
|
+
<span class="ngb-kbd">></span><span class="mr-3">Commands</span>
|
|
28
|
+
<span class="ngb-kbd">/</span><span class="mr-3">Pages</span>
|
|
29
|
+
<span class="ngb-kbd">#</span><span class="mr-3">Reports</span>
|
|
30
|
+
<span class="ngb-kbd">:</span><span class="mr-3">Documents</span>
|
|
31
|
+
<span class="ngb-kbd">@</span><span>Catalogs</span>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<template v-else>
|
|
36
|
+
<div class="text-sm font-semibold text-ngb-text">No results for “{{ query }}”</div>
|
|
37
|
+
<div class="mt-2 text-sm text-ngb-muted">Try a document number, a page name, or a scope prefix to narrow the search.</div>
|
|
38
|
+
<div class="mt-4 flex flex-wrap gap-2 text-xs text-ngb-muted">
|
|
39
|
+
<span class="ngb-kbd">></span><span class="mr-3">Commands</span>
|
|
40
|
+
<span class="ngb-kbd">/</span><span class="mr-3">Pages</span>
|
|
41
|
+
<span class="ngb-kbd">#</span><span class="mr-3">Reports</span>
|
|
42
|
+
<span class="ngb-kbd">:</span><span class="mr-3">Documents</span>
|
|
43
|
+
<span class="ngb-kbd">@</span><span>Catalogs</span>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
defineProps<{
|
|
51
|
+
query: string
|
|
52
|
+
loading: boolean
|
|
53
|
+
error?: string | null
|
|
54
|
+
}>()
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-ngb-border px-4 py-3 text-xs text-ngb-muted">
|
|
3
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
4
|
+
<span class="ngb-kbd">↑</span>
|
|
5
|
+
<span class="ngb-kbd">↓</span>
|
|
6
|
+
<span>Navigate</span>
|
|
7
|
+
<span class="ngb-kbd">Enter</span>
|
|
8
|
+
<span>Open</span>
|
|
9
|
+
<span class="ngb-kbd">{{ primaryModifier }}</span>
|
|
10
|
+
<span class="ngb-kbd">Enter</span>
|
|
11
|
+
<span>New tab</span>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
15
|
+
<span class="ngb-kbd">Esc</span>
|
|
16
|
+
<span>Close</span>
|
|
17
|
+
<span class="ngb-kbd">{{ primaryModifier }}</span>
|
|
18
|
+
<span class="ngb-kbd">K</span>
|
|
19
|
+
<span>Focus</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
defineProps<{
|
|
26
|
+
primaryModifier: string
|
|
27
|
+
}>()
|
|
28
|
+
</script>
|
|
29
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { httpPost } from '../api/http'
|
|
2
|
+
import type { CommandPaletteSearchRequestDto, CommandPaletteSearchResponseDto } from './types'
|
|
3
|
+
|
|
4
|
+
export async function searchCommandPalette(
|
|
5
|
+
request: CommandPaletteSearchRequestDto,
|
|
6
|
+
signal?: AbortSignal,
|
|
7
|
+
): Promise<CommandPaletteSearchResponseDto> {
|
|
8
|
+
return await httpPost<CommandPaletteSearchResponseDto>('/api/search/command-palette', request, { signal })
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CommandPaletteStoreConfig } from './types'
|
|
2
|
+
|
|
3
|
+
let commandPaletteConfig: CommandPaletteStoreConfig | null = null
|
|
4
|
+
|
|
5
|
+
export function configureNgbCommandPalette(config: CommandPaletteStoreConfig): void {
|
|
6
|
+
commandPaletteConfig = config
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getConfiguredNgbCommandPalette(): CommandPaletteStoreConfig {
|
|
10
|
+
if (!commandPaletteConfig) {
|
|
11
|
+
throw new Error('NGB command palette is not configured. Call configureNgbCommandPalette(...) during app bootstrap.')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return commandPaletteConfig
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { coerceNgbIconName, type NgbIconName } from '../primitives/iconNames'
|
|
2
|
+
import type { CommandPaletteItem } from './types'
|
|
3
|
+
|
|
4
|
+
const GENERIC_BADGES = new Set(['Favorite', 'Recent'])
|
|
5
|
+
const GENERIC_SUBTITLES = new Set([
|
|
6
|
+
'Favorite',
|
|
7
|
+
'Favorite report',
|
|
8
|
+
'Recent',
|
|
9
|
+
'Report',
|
|
10
|
+
'Page',
|
|
11
|
+
'Document',
|
|
12
|
+
'Catalog',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
function cleanMeta(value: string | null | undefined): string | null {
|
|
16
|
+
const next = String(value ?? '').trim()
|
|
17
|
+
return next.length > 0 ? next : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveCommandPaletteIcon(icon: string | null | undefined): NgbIconName {
|
|
21
|
+
return coerceNgbIconName(icon, 'search')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCommandPaletteBadge(item: CommandPaletteItem): string | null {
|
|
25
|
+
const badge = cleanMeta(item.badge)
|
|
26
|
+
if (badge && !GENERIC_BADGES.has(badge)) return badge
|
|
27
|
+
|
|
28
|
+
switch (item.kind) {
|
|
29
|
+
case 'page':
|
|
30
|
+
return 'Page'
|
|
31
|
+
case 'document':
|
|
32
|
+
return 'Document'
|
|
33
|
+
case 'catalog':
|
|
34
|
+
return 'Catalog'
|
|
35
|
+
case 'report':
|
|
36
|
+
return 'Report'
|
|
37
|
+
case 'command':
|
|
38
|
+
return 'Action'
|
|
39
|
+
case 'recent':
|
|
40
|
+
switch (item.scope) {
|
|
41
|
+
case 'reports':
|
|
42
|
+
return 'Report'
|
|
43
|
+
case 'documents':
|
|
44
|
+
return 'Document'
|
|
45
|
+
case 'catalogs':
|
|
46
|
+
return 'Catalog'
|
|
47
|
+
default:
|
|
48
|
+
return 'Page'
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveCommandPaletteSubtitle(item: CommandPaletteItem): string {
|
|
56
|
+
const subtitle = cleanMeta(item.subtitle)
|
|
57
|
+
if (subtitle && !GENERIC_SUBTITLES.has(subtitle)) return subtitle
|
|
58
|
+
|
|
59
|
+
switch (item.kind) {
|
|
60
|
+
case 'page':
|
|
61
|
+
return 'Open this page'
|
|
62
|
+
case 'document':
|
|
63
|
+
return 'Open this document'
|
|
64
|
+
case 'catalog':
|
|
65
|
+
return 'Open this catalog record'
|
|
66
|
+
case 'report':
|
|
67
|
+
return 'Run this report'
|
|
68
|
+
case 'command':
|
|
69
|
+
return 'Run this command'
|
|
70
|
+
case 'recent':
|
|
71
|
+
switch (item.scope) {
|
|
72
|
+
case 'reports':
|
|
73
|
+
return 'Open this recent report'
|
|
74
|
+
case 'documents':
|
|
75
|
+
return 'Open this recent document'
|
|
76
|
+
case 'catalogs':
|
|
77
|
+
return 'Open this recent catalog record'
|
|
78
|
+
default:
|
|
79
|
+
return 'Open this recent page'
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
return 'Open this item'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { CommandPaletteGroupCode, CommandPaletteScope } from './types'
|
|
2
|
+
|
|
3
|
+
type SearchField = {
|
|
4
|
+
value?: string | null
|
|
5
|
+
exact: number
|
|
6
|
+
prefix: number
|
|
7
|
+
wordPrefix: number
|
|
8
|
+
contains: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeSearchText(value: string | null | undefined): string {
|
|
12
|
+
const source = String(value ?? '').trim().toLowerCase()
|
|
13
|
+
if (!source) return ''
|
|
14
|
+
|
|
15
|
+
let out = ''
|
|
16
|
+
let lastWasSpace = false
|
|
17
|
+
|
|
18
|
+
for (const char of source) {
|
|
19
|
+
if (/[a-z0-9]/.test(char)) {
|
|
20
|
+
out += char
|
|
21
|
+
lastWasSpace = false
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (lastWasSpace) continue
|
|
26
|
+
out += ' '
|
|
27
|
+
lastWasSpace = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return out.trim()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseCommandPaletteQuery(value: string): { rawQuery: string; query: string; scope: CommandPaletteScope | null } {
|
|
34
|
+
const rawQuery = String(value ?? '')
|
|
35
|
+
const trimmed = rawQuery.trimStart()
|
|
36
|
+
const prefix = trimmed.charAt(0)
|
|
37
|
+
|
|
38
|
+
const scope = prefixToScope(prefix)
|
|
39
|
+
if (!scope) {
|
|
40
|
+
return {
|
|
41
|
+
rawQuery,
|
|
42
|
+
query: rawQuery.trim(),
|
|
43
|
+
scope: null,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
rawQuery,
|
|
49
|
+
query: trimmed.slice(1).trim(),
|
|
50
|
+
scope,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function prefixToScope(prefix: string): CommandPaletteScope | null {
|
|
55
|
+
switch (prefix) {
|
|
56
|
+
case '>':
|
|
57
|
+
return 'commands'
|
|
58
|
+
case '/':
|
|
59
|
+
return 'pages'
|
|
60
|
+
case '#':
|
|
61
|
+
return 'reports'
|
|
62
|
+
case ':':
|
|
63
|
+
return 'documents'
|
|
64
|
+
case '@':
|
|
65
|
+
return 'catalogs'
|
|
66
|
+
default:
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function scoreSearchText(query: string, fields: SearchField[]): number {
|
|
72
|
+
const normalizedQuery = normalizeSearchText(query)
|
|
73
|
+
if (!normalizedQuery) return 0
|
|
74
|
+
|
|
75
|
+
let best = 0
|
|
76
|
+
for (const field of fields) {
|
|
77
|
+
best = Math.max(best, scoreField(normalizedQuery, field))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return best
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function groupOrder(code: CommandPaletteGroupCode): number {
|
|
84
|
+
switch (code) {
|
|
85
|
+
case 'actions':
|
|
86
|
+
return 0
|
|
87
|
+
case 'go-to':
|
|
88
|
+
return 1
|
|
89
|
+
case 'documents':
|
|
90
|
+
return 2
|
|
91
|
+
case 'catalogs':
|
|
92
|
+
return 3
|
|
93
|
+
case 'reports':
|
|
94
|
+
return 4
|
|
95
|
+
case 'recent':
|
|
96
|
+
return 5
|
|
97
|
+
default:
|
|
98
|
+
return 99
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defaultSearchFields(...values: Array<string | null | undefined>): SearchField[] {
|
|
103
|
+
return values
|
|
104
|
+
.filter((value) => String(value ?? '').trim().length > 0)
|
|
105
|
+
.map((value, index) => ({
|
|
106
|
+
value,
|
|
107
|
+
exact: index === 0 ? 1 : 0.86,
|
|
108
|
+
prefix: index === 0 ? 0.92 : 0.8,
|
|
109
|
+
wordPrefix: index === 0 ? 0.88 : 0.76,
|
|
110
|
+
contains: index === 0 ? 0.76 : 0.68,
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function scoreField(normalizedQuery: string, field: SearchField): number {
|
|
115
|
+
const normalizedValue = normalizeSearchText(field.value)
|
|
116
|
+
if (!normalizedValue) return 0
|
|
117
|
+
|
|
118
|
+
if (normalizedValue === normalizedQuery) return field.exact
|
|
119
|
+
if (normalizedValue.startsWith(normalizedQuery)) return field.prefix
|
|
120
|
+
if (containsWordPrefix(normalizedValue, normalizedQuery)) return field.wordPrefix
|
|
121
|
+
if (normalizedValue.includes(normalizedQuery)) return field.contains
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function containsWordPrefix(value: string, query: string): boolean {
|
|
127
|
+
const parts = value.split(' ')
|
|
128
|
+
return parts.some((part) => part.startsWith(query))
|
|
129
|
+
}
|
|
130
|
+
|