@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,66 @@
|
|
|
1
|
+
import type { RouteLocationNormalizedLoaded, RouteLocationRaw, RouteRecordRaw } from 'vue-router'
|
|
2
|
+
import { buildGeneralJournalEntriesListPath, buildGeneralJournalEntriesPath } from '../accounting/navigation'
|
|
3
|
+
import { buildReportPageUrl } from '../reporting/navigation'
|
|
4
|
+
|
|
5
|
+
type AliasRouteLike = Pick<RouteLocationNormalizedLoaded, 'params' | 'query' | 'hash'>
|
|
6
|
+
|
|
7
|
+
function preserveAliasContext(to: AliasRouteLike, path: string): RouteLocationRaw {
|
|
8
|
+
return {
|
|
9
|
+
path,
|
|
10
|
+
query: to.query,
|
|
11
|
+
hash: to.hash,
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ngbRouteAliasRedirectRoutes: RouteRecordRaw[] = [
|
|
16
|
+
{
|
|
17
|
+
path: '/documents/general_journal_entry',
|
|
18
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesListPath()),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: '/documents/general_journal_entry/new',
|
|
22
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath()),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: '/documents/general_journal_entry/:id',
|
|
26
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath(String(to.params.id ?? ''))),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '/documents/accounting.general_journal_entry',
|
|
30
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesListPath()),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: '/documents/accounting.general_journal_entry/new',
|
|
34
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath()),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: '/documents/accounting.general_journal_entry/:id',
|
|
38
|
+
redirect: to => preserveAliasContext(to, buildGeneralJournalEntriesPath(String(to.params.id ?? ''))),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: '/admin/accounting/posting-log',
|
|
42
|
+
redirect: to => preserveAliasContext(to, buildReportPageUrl('accounting.posting_log')),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: '/admin/accounting/consistency',
|
|
46
|
+
redirect: to => preserveAliasContext(to, buildReportPageUrl('accounting.consistency')),
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
export function normalizeNgbRouteAliasPath(path: string | null | undefined): string {
|
|
51
|
+
const value = String(path ?? '').trim()
|
|
52
|
+
if (!value) return ''
|
|
53
|
+
|
|
54
|
+
if (value.startsWith('/documents/accounting.general_journal_entry')) {
|
|
55
|
+
return value.replace('/documents/accounting.general_journal_entry', '/accounting/general-journal-entries')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (value.startsWith('/documents/general_journal_entry')) {
|
|
59
|
+
return value.replace('/documents/general_journal_entry', '/accounting/general-journal-entries')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (value === '/admin/accounting/posting-log') return buildReportPageUrl('accounting.posting_log')
|
|
63
|
+
if (value === '/admin/accounting/consistency') return buildReportPageUrl('accounting.consistency')
|
|
64
|
+
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeRouteParam(value: unknown): string | null {
|
|
2
|
+
const raw = Array.isArray(value) ? value[0] : value
|
|
3
|
+
const normalized = String(raw ?? '').trim()
|
|
4
|
+
return normalized || null
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeRequiredRouteParam(value: unknown): string {
|
|
8
|
+
return normalizeRouteParam(value) ?? ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeEntityEditorIdRouteParam(value: unknown): string | undefined {
|
|
12
|
+
const normalized = normalizeRouteParam(value)
|
|
13
|
+
if (!normalized) return undefined
|
|
14
|
+
return normalized.toLowerCase() === 'new' ? undefined : normalized
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RouteLocationRaw, Router } from 'vue-router'
|
|
2
|
+
import type { ToastApi } from '../primitives/toast'
|
|
3
|
+
import { toErrorMessage } from '../utils/errorMessage'
|
|
4
|
+
|
|
5
|
+
type ToastsLike = Pick<ToastApi, 'push'>
|
|
6
|
+
|
|
7
|
+
export function buildAbsoluteAppUrl(router: Router, to: RouteLocationRaw): string {
|
|
8
|
+
const href = router.resolve(to).href
|
|
9
|
+
if (typeof window !== 'undefined' && window.location?.origin) {
|
|
10
|
+
return new URL(href, window.location.origin).toString()
|
|
11
|
+
}
|
|
12
|
+
return href
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function copyAppLink(
|
|
16
|
+
router: Router,
|
|
17
|
+
toasts: ToastsLike,
|
|
18
|
+
to: RouteLocationRaw,
|
|
19
|
+
opts?: { title?: string; message?: string },
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
const url = buildAbsoluteAppUrl(router, to)
|
|
23
|
+
if (!navigator?.clipboard?.writeText) throw new Error('Clipboard is not available.')
|
|
24
|
+
await navigator.clipboard.writeText(url)
|
|
25
|
+
toasts.push({
|
|
26
|
+
title: opts?.title ?? 'Link copied',
|
|
27
|
+
message: opts?.message ?? 'Shareable link copied to clipboard.',
|
|
28
|
+
tone: 'neutral',
|
|
29
|
+
})
|
|
30
|
+
return true
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
toasts.push({
|
|
33
|
+
title: 'Could not copy link',
|
|
34
|
+
message: toErrorMessage(cause, 'Clipboard is not available.'),
|
|
35
|
+
tone: 'danger',
|
|
36
|
+
})
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
3
|
+
<h2 class="text-sm font-semibold text-ngb-text">Security audit</h2>
|
|
4
|
+
<div class="mt-3 space-y-2">
|
|
5
|
+
<div
|
|
6
|
+
v-for="event in events"
|
|
7
|
+
:key="event.id"
|
|
8
|
+
class="grid grid-cols-[minmax(0,1fr),auto] gap-3 border-t border-ngb-border pt-2 first:border-t-0 first:pt-0"
|
|
9
|
+
>
|
|
10
|
+
<div class="min-w-0">
|
|
11
|
+
<div class="truncate text-sm text-ngb-text">{{ event.title }}</div>
|
|
12
|
+
<div v-if="event.actor" class="mt-0.5 truncate text-xs text-ngb-muted">{{ event.actor }}</div>
|
|
13
|
+
</div>
|
|
14
|
+
<time class="text-xs text-ngb-muted">{{ event.occurredAt }}</time>
|
|
15
|
+
</div>
|
|
16
|
+
<div v-if="events.length === 0" class="text-sm text-ngb-muted">No security audit events in this view.</div>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
withDefaults(defineProps<{
|
|
23
|
+
events?: Array<{
|
|
24
|
+
id: string
|
|
25
|
+
title: string
|
|
26
|
+
actor?: string | null
|
|
27
|
+
occurredAt: string
|
|
28
|
+
}>
|
|
29
|
+
}>(), {
|
|
30
|
+
events: () => [],
|
|
31
|
+
})
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full min-h-[320px] items-center justify-center p-6">
|
|
3
|
+
<div class="w-full max-w-[520px] rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-center shadow-card">
|
|
4
|
+
<div class="mx-auto flex h-11 w-11 items-center justify-center rounded-full border border-ngb-border bg-ngb-bg text-ngb-muted">
|
|
5
|
+
<NgbIcon name="shield" :size="22" />
|
|
6
|
+
</div>
|
|
7
|
+
<h1 class="mt-4 text-lg font-semibold text-ngb-text">{{ title }}</h1>
|
|
8
|
+
<p class="mt-2 text-sm leading-6 text-ngb-muted">{{ message }}</p>
|
|
9
|
+
<div v-if="$slots.actions" class="mt-5 flex justify-center gap-2">
|
|
10
|
+
<slot name="actions" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
18
|
+
|
|
19
|
+
withDefaults(defineProps<{
|
|
20
|
+
title?: string
|
|
21
|
+
message?: string
|
|
22
|
+
}>(), {
|
|
23
|
+
title: 'Access denied',
|
|
24
|
+
message: 'Your current access profile does not allow this operation.',
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="flex min-w-0 flex-col rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card" data-testid="effective-access-panel">
|
|
3
|
+
<div class="flex items-center justify-between gap-3 border-b border-ngb-border px-4 py-3">
|
|
4
|
+
<div class="min-w-0">
|
|
5
|
+
<h2 class="truncate text-sm font-semibold text-ngb-text">Effective access</h2>
|
|
6
|
+
<div class="mt-0.5 text-xs text-ngb-muted">Version {{ access?.accessVersion ?? '-' }}</div>
|
|
7
|
+
</div>
|
|
8
|
+
<NgbButton v-if="showRefresh" size="sm" variant="ghost" :disabled="loading" @click="$emit('refresh')">
|
|
9
|
+
<NgbIcon name="refresh" :size="15" />
|
|
10
|
+
Refresh
|
|
11
|
+
</NgbButton>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div v-if="loading" class="p-4 text-sm text-ngb-muted">Loading...</div>
|
|
15
|
+
<div v-else-if="error" class="p-4 text-sm text-ngb-danger">{{ error }}</div>
|
|
16
|
+
<div v-else-if="!access || access.groups.length === 0" class="p-4 text-sm text-ngb-muted">No effective permissions.</div>
|
|
17
|
+
<div v-else class="min-h-0 flex-1 overflow-auto">
|
|
18
|
+
<section v-for="group in access.groups" :key="group.group" class="border-b border-ngb-border last:border-b-0">
|
|
19
|
+
<div class="bg-[var(--ngb-grid-header)] px-4 py-2 text-xs font-semibold uppercase tracking-wide text-ngb-muted">
|
|
20
|
+
{{ group.group }}
|
|
21
|
+
</div>
|
|
22
|
+
<div class="divide-y divide-ngb-border">
|
|
23
|
+
<div
|
|
24
|
+
v-for="resource in group.resources"
|
|
25
|
+
:key="`${resource.resourceKind}:${resource.resourceCode}`"
|
|
26
|
+
class="grid grid-cols-[minmax(0,1fr),minmax(160px,auto)] gap-3 px-4 py-3"
|
|
27
|
+
>
|
|
28
|
+
<div class="min-w-0">
|
|
29
|
+
<div class="truncate text-sm font-medium text-ngb-text">{{ resource.displayName }}</div>
|
|
30
|
+
<div class="mt-0.5 truncate font-mono text-[11px] text-ngb-muted">{{ resource.resourceKind }}.{{ resource.resourceCode }}</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex flex-wrap justify-end gap-1.5">
|
|
33
|
+
<NgbBadge v-for="action in resource.actions" :key="action" tone="neutral">{{ action }}</NgbBadge>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import NgbBadge from '../primitives/NgbBadge.vue'
|
|
44
|
+
import NgbButton from '../primitives/NgbButton.vue'
|
|
45
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
46
|
+
import type { EffectiveAccessDto } from './types'
|
|
47
|
+
|
|
48
|
+
withDefaults(defineProps<{
|
|
49
|
+
access: EffectiveAccessDto | null
|
|
50
|
+
loading?: boolean
|
|
51
|
+
error?: string | null
|
|
52
|
+
showRefresh?: boolean
|
|
53
|
+
}>(), {
|
|
54
|
+
showRefresh: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
defineEmits<{
|
|
58
|
+
(e: 'refresh'): void
|
|
59
|
+
}>()
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-w-0 space-y-4" data-testid="permission-matrix">
|
|
3
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
4
|
+
<NgbInput
|
|
5
|
+
v-model="query"
|
|
6
|
+
class="max-w-[360px]"
|
|
7
|
+
placeholder="Filter permissions"
|
|
8
|
+
:disabled="disabled"
|
|
9
|
+
/>
|
|
10
|
+
<div class="text-xs text-ngb-muted tabular-nums">{{ selectedCount }} / {{ definitions.length }}</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div v-if="groups.length === 0" class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-6 text-sm text-ngb-muted">
|
|
14
|
+
No permissions match the current filter.
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<section
|
|
18
|
+
v-for="group in groups"
|
|
19
|
+
:key="group.group"
|
|
20
|
+
class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card"
|
|
21
|
+
>
|
|
22
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-ngb-border px-4 py-3">
|
|
23
|
+
<div class="min-w-0">
|
|
24
|
+
<h2 class="truncate text-sm font-semibold text-ngb-text">{{ group.group }}</h2>
|
|
25
|
+
<div class="mt-0.5 text-xs text-ngb-muted tabular-nums">{{ selectedInGroup(group.permissions) }} / {{ group.permissions.length }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
<NgbButton
|
|
28
|
+
size="sm"
|
|
29
|
+
variant="ghost"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
@click="toggleGroup(group.permissions)"
|
|
32
|
+
>
|
|
33
|
+
<NgbIcon :name="isGroupFullySelected(group.permissions) ? 'minus' : 'check'" :size="15" />
|
|
34
|
+
{{ isGroupFullySelected(group.permissions) ? 'Clear' : 'Select' }}
|
|
35
|
+
</NgbButton>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="divide-y divide-ngb-border">
|
|
39
|
+
<label
|
|
40
|
+
v-for="permission in group.permissions"
|
|
41
|
+
:key="permissionKey(permission)"
|
|
42
|
+
class="grid cursor-pointer grid-cols-[1.25rem,minmax(0,1fr),auto] items-start gap-3 px-4 py-3 hover:bg-[var(--ngb-row-hover)]"
|
|
43
|
+
:class="disabled ? 'cursor-not-allowed opacity-70' : ''"
|
|
44
|
+
>
|
|
45
|
+
<input
|
|
46
|
+
type="checkbox"
|
|
47
|
+
class="mt-1 h-4 w-4 rounded-none border-ngb-border"
|
|
48
|
+
:checked="selectedKeys.has(permissionKey(permission))"
|
|
49
|
+
:disabled="disabled"
|
|
50
|
+
@change="setPermission(permission, ($event.target as HTMLInputElement).checked)"
|
|
51
|
+
/>
|
|
52
|
+
<span class="min-w-0">
|
|
53
|
+
<span class="block text-sm font-medium text-ngb-text">{{ permission.displayName }}</span>
|
|
54
|
+
<span v-if="permission.description" class="mt-0.5 block text-xs leading-5 text-ngb-muted">{{ permission.description }}</span>
|
|
55
|
+
<span class="mt-1 block truncate font-mono text-[11px] text-ngb-muted">{{ permissionKey(permission) }}</span>
|
|
56
|
+
</span>
|
|
57
|
+
<span class="mt-0.5 rounded-[var(--ngb-radius)] border border-ngb-border px-2 py-0.5 text-[11px] text-ngb-muted">
|
|
58
|
+
{{ permission.actionCode }}
|
|
59
|
+
</span>
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { computed, ref } from 'vue'
|
|
68
|
+
import NgbButton from '../primitives/NgbButton.vue'
|
|
69
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
70
|
+
import NgbInput from '../primitives/NgbInput.vue'
|
|
71
|
+
import { buildPermissionKey, groupPermissionDefinitions } from './permissions'
|
|
72
|
+
import type { PermissionAssignmentDto, PermissionDefinitionDto } from './types'
|
|
73
|
+
|
|
74
|
+
const props = withDefaults(defineProps<{
|
|
75
|
+
modelValue: PermissionAssignmentDto[]
|
|
76
|
+
definitions: PermissionDefinitionDto[]
|
|
77
|
+
disabled?: boolean
|
|
78
|
+
}>(), {
|
|
79
|
+
disabled: false,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const emit = defineEmits<{
|
|
83
|
+
(e: 'update:modelValue', value: PermissionAssignmentDto[]): void
|
|
84
|
+
}>()
|
|
85
|
+
|
|
86
|
+
const query = ref('')
|
|
87
|
+
|
|
88
|
+
const selectedKeys = computed(() => new Set((props.modelValue ?? []).map((entry) => buildPermissionKey(entry))))
|
|
89
|
+
const selectedCount = computed(() => selectedKeys.value.size)
|
|
90
|
+
|
|
91
|
+
const filteredDefinitions = computed(() => {
|
|
92
|
+
const text = query.value.trim().toLowerCase()
|
|
93
|
+
const definitions = props.definitions ?? []
|
|
94
|
+
if (!text) return definitions
|
|
95
|
+
|
|
96
|
+
return definitions.filter((permission) => [
|
|
97
|
+
permission.displayName,
|
|
98
|
+
permission.description ?? '',
|
|
99
|
+
permission.group,
|
|
100
|
+
permission.resourceKind,
|
|
101
|
+
permission.resourceCode,
|
|
102
|
+
permission.actionCode,
|
|
103
|
+
buildPermissionKey(permission),
|
|
104
|
+
].join(' ').toLowerCase().includes(text))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const groups = computed(() => groupPermissionDefinitions(filteredDefinitions.value))
|
|
108
|
+
|
|
109
|
+
function permissionKey(permission: PermissionAssignmentDto): string {
|
|
110
|
+
return buildPermissionKey(permission)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeAssignments(keys: Set<string>): PermissionAssignmentDto[] {
|
|
114
|
+
const byKey = new Map(props.definitions.map((definition) => [permissionKey(definition), definition]))
|
|
115
|
+
return Array.from(keys)
|
|
116
|
+
.sort()
|
|
117
|
+
.map((key) => byKey.get(key))
|
|
118
|
+
.filter((entry): entry is PermissionDefinitionDto => !!entry)
|
|
119
|
+
.map((entry) => ({
|
|
120
|
+
resourceKind: entry.resourceKind,
|
|
121
|
+
resourceCode: entry.resourceCode,
|
|
122
|
+
actionCode: entry.actionCode,
|
|
123
|
+
}))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setPermission(permission: PermissionAssignmentDto, checked: boolean): void {
|
|
127
|
+
if (props.disabled) return
|
|
128
|
+
|
|
129
|
+
const keys = new Set(selectedKeys.value)
|
|
130
|
+
const key = permissionKey(permission)
|
|
131
|
+
if (checked) keys.add(key)
|
|
132
|
+
else keys.delete(key)
|
|
133
|
+
|
|
134
|
+
emit('update:modelValue', normalizeAssignments(keys))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isGroupFullySelected(permissions: PermissionDefinitionDto[]): boolean {
|
|
138
|
+
return permissions.length > 0 && permissions.every((permission) => selectedKeys.value.has(permissionKey(permission)))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function selectedInGroup(permissions: PermissionDefinitionDto[]): number {
|
|
142
|
+
return permissions.filter((permission) => selectedKeys.value.has(permissionKey(permission))).length
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toggleGroup(permissions: PermissionDefinitionDto[]): void {
|
|
146
|
+
if (props.disabled) return
|
|
147
|
+
|
|
148
|
+
const keys = new Set(selectedKeys.value)
|
|
149
|
+
const selected = isGroupFullySelected(permissions)
|
|
150
|
+
|
|
151
|
+
for (const permission of permissions) {
|
|
152
|
+
const key = permissionKey(permission)
|
|
153
|
+
if (selected) keys.delete(key)
|
|
154
|
+
else keys.add(key)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
emit('update:modelValue', normalizeAssignments(keys))
|
|
158
|
+
}
|
|
159
|
+
</script>
|
|
160
|
+
|