@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,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import NgbConfirmDialog from '../components/NgbConfirmDialog.vue';
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
open: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
confirmText?: string;
|
|
9
|
+
cancelText?: string;
|
|
10
|
+
}>(), {
|
|
11
|
+
title: 'Discard changes?',
|
|
12
|
+
message: 'You have unsaved changes. If you close this panel now, they won’t be saved.',
|
|
13
|
+
confirmText: 'Discard',
|
|
14
|
+
cancelText: 'Keep editing',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: 'update:open', value: boolean): void;
|
|
19
|
+
(e: 'confirm'): void;
|
|
20
|
+
(e: 'cancel'): void;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
function handleOpenUpdate(value: boolean) {
|
|
24
|
+
emit('update:open', value);
|
|
25
|
+
if (!value) emit('cancel');
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<NgbConfirmDialog
|
|
31
|
+
:open="props.open"
|
|
32
|
+
:title="props.title"
|
|
33
|
+
:message="props.message"
|
|
34
|
+
:confirm-text="props.confirmText"
|
|
35
|
+
:cancel-text="props.cancelText"
|
|
36
|
+
danger
|
|
37
|
+
@update:open="handleOpenUpdate"
|
|
38
|
+
@confirm="emit('confirm')"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
import { toErrorMessage } from '../utils/errorMessage';
|
|
5
|
+
import { stableStringify } from '../utils/stableValue';
|
|
6
|
+
import NgbIcon from '../primitives/NgbIcon.vue';
|
|
7
|
+
|
|
8
|
+
import { getConfiguredNgbEditor, resolveNgbEditorAuditBehavior } from './config';
|
|
9
|
+
import type { EditorAuditBehavior } from './types';
|
|
10
|
+
import type { AuditEvent } from './types';
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
open: boolean;
|
|
14
|
+
entityKind: number;
|
|
15
|
+
entityId?: string | null;
|
|
16
|
+
entityTitle?: string | null;
|
|
17
|
+
behavior?: EditorAuditBehavior;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: 'back'): void;
|
|
22
|
+
(e: 'close'): void;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const loading = ref(false);
|
|
26
|
+
const error = ref<string | null>(null);
|
|
27
|
+
const items = ref<AuditEvent[]>([]);
|
|
28
|
+
let loadSequence = 0;
|
|
29
|
+
|
|
30
|
+
const canLoad = computed(() => !!props.entityId);
|
|
31
|
+
const title = computed(() => props.entityTitle?.trim() || 'Audit Log');
|
|
32
|
+
const behavior = computed(() => resolveNgbEditorAuditBehavior(props.behavior));
|
|
33
|
+
const hiddenFieldNames = computed(() => new Set((behavior.value.hiddenFieldNames ?? []).map((item) => item.toLowerCase())));
|
|
34
|
+
const explicitFieldLabels = computed(() => {
|
|
35
|
+
const entries = Object.entries(behavior.value.explicitFieldLabels ?? {});
|
|
36
|
+
return Object.fromEntries(entries.map(([key, value]) => [key.toLowerCase(), value]));
|
|
37
|
+
});
|
|
38
|
+
const explicitActionTitles = computed(() => {
|
|
39
|
+
const entries = Object.entries(behavior.value.actionTitles ?? {});
|
|
40
|
+
return Object.fromEntries(entries.map(([key, value]) => [key.toLowerCase(), value]));
|
|
41
|
+
});
|
|
42
|
+
type AuditRow = { field: string; before: string; after: string };
|
|
43
|
+
|
|
44
|
+
function formatDateTime(v: string): string {
|
|
45
|
+
const d = new Date(v);
|
|
46
|
+
if (Number.isNaN(d.getTime())) return v;
|
|
47
|
+
return d.toLocaleString([], {
|
|
48
|
+
year: 'numeric',
|
|
49
|
+
month: '2-digit',
|
|
50
|
+
day: '2-digit',
|
|
51
|
+
hour: '2-digit',
|
|
52
|
+
minute: '2-digit',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function actorLabel(e: AuditEvent): string {
|
|
57
|
+
const a = e.actor;
|
|
58
|
+
if (!a) return 'System';
|
|
59
|
+
return a.displayName?.trim() || a.email?.trim() || 'System';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function humanizeField(fieldPath: string): string {
|
|
63
|
+
const raw = String(fieldPath ?? '');
|
|
64
|
+
const last = raw.split('.').pop() || raw;
|
|
65
|
+
const explicit = explicitFieldLabels.value[last.toLowerCase()];
|
|
66
|
+
if (explicit) return explicit;
|
|
67
|
+
return last
|
|
68
|
+
.replace(/_/g, ' ')
|
|
69
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
70
|
+
.replace(/\bid\b/gi, 'ID')
|
|
71
|
+
.replace(/\butc\b/gi, 'UTC')
|
|
72
|
+
.replace(/\b\w/g, (m) => m.toUpperCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function humanizeLooseText(v: string): string {
|
|
76
|
+
return String(v ?? '')
|
|
77
|
+
.replace(/[._]/g, ' ')
|
|
78
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
79
|
+
.replace(/\b\w/g, (m) => m.toUpperCase())
|
|
80
|
+
.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseJsonLoose(v?: string | null): unknown {
|
|
84
|
+
if (v == null || v === '') return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(v);
|
|
87
|
+
} catch {
|
|
88
|
+
return v;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function looksLikeIsoDateTime(v: string): boolean {
|
|
97
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function looksLikeDateOnly(v: string): boolean {
|
|
101
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatPrimitiveString(v: string): string {
|
|
105
|
+
const trimmed = v.trim();
|
|
106
|
+
if (!trimmed) return '—';
|
|
107
|
+
|
|
108
|
+
if (looksLikeIsoDateTime(trimmed)) return formatDateTime(trimmed);
|
|
109
|
+
|
|
110
|
+
if (looksLikeDateOnly(trimmed)) {
|
|
111
|
+
const d = new Date(`${trimmed}T00:00:00`);
|
|
112
|
+
if (!Number.isNaN(d.getTime())) return d.toLocaleDateString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (/^(true|false)$/i.test(trimmed)) {
|
|
116
|
+
return trimmed.toLowerCase() === 'true' ? 'Yes' : 'No';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/^[a-z]+([A-Z][a-z0-9]+)+$/.test(trimmed) || trimmed.includes('_')) {
|
|
120
|
+
return humanizeLooseText(trimmed);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return trimmed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatAuditValue(parsed: unknown): string {
|
|
127
|
+
if (parsed == null || parsed === '') return '—';
|
|
128
|
+
if (typeof parsed === 'boolean') return parsed ? 'Yes' : 'No';
|
|
129
|
+
if (typeof parsed === 'number') return String(parsed);
|
|
130
|
+
if (typeof parsed === 'string') return formatPrimitiveString(parsed);
|
|
131
|
+
if (Array.isArray(parsed)) {
|
|
132
|
+
if (parsed.length === 0) return '—';
|
|
133
|
+
return parsed.map((entry) => formatAuditValue(entry)).join(', ');
|
|
134
|
+
}
|
|
135
|
+
if (isRecord(parsed)) {
|
|
136
|
+
for (const key of ['display', 'label', 'name', 'email', 'id']) {
|
|
137
|
+
const value = parsed[key];
|
|
138
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
139
|
+
}
|
|
140
|
+
const permissionDisplay = formatPermissionLikeRecord(parsed);
|
|
141
|
+
if (permissionDisplay) return permissionDisplay;
|
|
142
|
+
return stableStringify(parsed);
|
|
143
|
+
}
|
|
144
|
+
return String(parsed);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function valueText(v?: string | null): string {
|
|
148
|
+
return formatAuditValue(parseJsonLoose(v));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function actionTitle(actionCode: string): string {
|
|
152
|
+
const code = String(actionCode ?? '').toLowerCase();
|
|
153
|
+
const explicitTitle = explicitActionTitles.value[code];
|
|
154
|
+
if (explicitTitle) return explicitTitle;
|
|
155
|
+
if (code.endsWith('.create') || code.includes('.create_')) return 'Created';
|
|
156
|
+
if (code.endsWith('.update') || code.includes('.update_') || code.includes('.replace_')) return 'Updated';
|
|
157
|
+
if (code.endsWith('.submit')) return 'Submitted';
|
|
158
|
+
if (code.endsWith('.approve')) return 'Approved';
|
|
159
|
+
if (code.endsWith('.reject')) return 'Rejected';
|
|
160
|
+
if (code.endsWith('.post')) return 'Posted';
|
|
161
|
+
if (code.endsWith('.unpost')) return 'Unposted';
|
|
162
|
+
if (code.endsWith('.repost')) return 'Reposted';
|
|
163
|
+
if (code.includes('unmark_for_deletion') || code.includes('restore')) return 'Restored';
|
|
164
|
+
if (code.includes('mark_for_deletion') || code.includes('soft_delete') || code.includes('.tombstone')) {
|
|
165
|
+
return 'Marked for deletion';
|
|
166
|
+
}
|
|
167
|
+
if (code.includes('set_active')) return 'Status changed';
|
|
168
|
+
return humanizeLooseText(actionCode);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function eventSummary(item: AuditEvent): string {
|
|
172
|
+
return `${actionTitle(item.actionCode)} by ${actorLabel(item)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sameText(a: string, b: string): boolean {
|
|
176
|
+
return a.trim() === b.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function eventRows(item: AuditEvent): AuditRow[] {
|
|
180
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
181
|
+
|
|
182
|
+
const rows = changes
|
|
183
|
+
.filter((change) => {
|
|
184
|
+
const key = String(change.fieldPath ?? '').split('.').pop()?.toLowerCase() ?? '';
|
|
185
|
+
return !hiddenFieldNames.value.has(key);
|
|
186
|
+
})
|
|
187
|
+
.map((change) => ({
|
|
188
|
+
field: humanizeField(change.fieldPath),
|
|
189
|
+
before: valueText(change.oldValueJson),
|
|
190
|
+
after: valueText(change.newValueJson),
|
|
191
|
+
}))
|
|
192
|
+
.filter((row) => !sameText(row.before, row.after));
|
|
193
|
+
|
|
194
|
+
if (rows.length > 0) return rows;
|
|
195
|
+
return [{ field: 'Details', before: '—', after: 'No business-field changes recorded.' }];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatPermissionLikeRecord(value: Record<string, unknown>): string | null {
|
|
199
|
+
const resourceCode = getString(value.resourceCode ?? value.resource);
|
|
200
|
+
const actionCode = getString(value.actionCode ?? value.action);
|
|
201
|
+
if (!resourceCode || !actionCode) return null;
|
|
202
|
+
|
|
203
|
+
return `${humanizeLooseText(resourceCode)}: ${humanizeLooseText(actionCode)}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getString(value: unknown): string | null {
|
|
207
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function load() {
|
|
211
|
+
if (!props.open || !props.entityId) return;
|
|
212
|
+
const seq = ++loadSequence;
|
|
213
|
+
loading.value = true;
|
|
214
|
+
error.value = null;
|
|
215
|
+
try {
|
|
216
|
+
const page = await getConfiguredNgbEditor().loadEntityAuditLog(props.entityKind, props.entityId, { limit: 100 });
|
|
217
|
+
if (seq !== loadSequence) return;
|
|
218
|
+
items.value = page.items ?? [];
|
|
219
|
+
} catch (cause) {
|
|
220
|
+
if (seq !== loadSequence) return;
|
|
221
|
+
error.value = toErrorMessage(cause, 'Failed to load the audit log.');
|
|
222
|
+
} finally {
|
|
223
|
+
if (seq === loadSequence) loading.value = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
watch(
|
|
228
|
+
() => [props.open, props.entityKind, props.entityId],
|
|
229
|
+
() => {
|
|
230
|
+
loadSequence += 1;
|
|
231
|
+
items.value = [];
|
|
232
|
+
error.value = null;
|
|
233
|
+
loading.value = false;
|
|
234
|
+
if (props.open && props.entityId) void load();
|
|
235
|
+
},
|
|
236
|
+
{ immediate: true },
|
|
237
|
+
);
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<div class="h-full min-h-0 flex flex-col bg-ngb-card">
|
|
242
|
+
<div class="px-5 py-4 border-b border-ngb-border flex items-start gap-3">
|
|
243
|
+
<div class="min-w-0 flex-1">
|
|
244
|
+
<div class="text-base font-semibold text-ngb-text truncate">Audit Log</div>
|
|
245
|
+
<div class="text-sm text-ngb-muted truncate mt-1">{{ title }}</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<button class="ngb-iconbtn shrink-0" title="Close" @click="emit('close')">
|
|
249
|
+
<NgbIcon name="x" />
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="flex-1 min-h-0 overflow-auto">
|
|
254
|
+
<div v-if="error" class="px-5 py-4 text-sm text-red-700">{{ error }}</div>
|
|
255
|
+
<div v-else-if="!canLoad" class="px-5 py-4 text-sm text-ngb-muted">Save the record first to see its history.</div>
|
|
256
|
+
<div v-else-if="loading && items.length === 0" class="px-5 py-4 text-sm text-ngb-muted">Loading…</div>
|
|
257
|
+
<div v-else-if="items.length === 0" class="px-5 py-4 text-sm text-ngb-muted">No history yet.</div>
|
|
258
|
+
|
|
259
|
+
<div v-else class="py-3">
|
|
260
|
+
<section v-for="item in items" :key="item.auditEventId" class="bg-ngb-card px-4 mb-4 last:mb-0">
|
|
261
|
+
<div class="py-3 border-b border-ngb-border flex items-center justify-between gap-4">
|
|
262
|
+
<div class="min-w-0 text-sm font-semibold text-ngb-text truncate">{{ eventSummary(item) }}</div>
|
|
263
|
+
<div class="shrink-0 text-sm text-ngb-text whitespace-nowrap">{{ formatDateTime(item.occurredAtUtc) }}</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="border border-ngb-border border-t-0">
|
|
267
|
+
<table class="w-full text-sm table-fixed">
|
|
268
|
+
<thead class="bg-ngb-bg text-ngb-muted text-xs">
|
|
269
|
+
<tr>
|
|
270
|
+
<th class="w-[34%] px-4 py-2 text-left font-semibold border-r border-ngb-border">Field</th>
|
|
271
|
+
<th class="w-[33%] px-4 py-2 text-left font-semibold border-r border-ngb-border">Original Value</th>
|
|
272
|
+
<th class="w-[33%] px-4 py-2 text-left font-semibold">New Value</th>
|
|
273
|
+
</tr>
|
|
274
|
+
</thead>
|
|
275
|
+
<tbody>
|
|
276
|
+
<tr v-for="row in eventRows(item)" :key="`${item.auditEventId}:${row.field}`" class="border-t border-ngb-border">
|
|
277
|
+
<td class="px-4 py-3 align-top border-r border-ngb-border break-words text-ngb-text">{{ row.field }}</td>
|
|
278
|
+
<td class="px-4 py-3 align-top border-r border-ngb-border break-words text-ngb-text">{{ row.before }}</td>
|
|
279
|
+
<td class="px-4 py-3 align-top break-words text-ngb-text">{{ row.after }}</td>
|
|
280
|
+
</tr>
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
</section>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</template>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
import NgbConfirmDialog from '../components/NgbConfirmDialog.vue';
|
|
5
|
+
import NgbDrawer from '../components/NgbDrawer.vue';
|
|
6
|
+
import NgbEntityForm from '../metadata/NgbEntityForm.vue';
|
|
7
|
+
import type { DocumentStatusValue, EntityFormModel, FormMetadata } from '../metadata/types';
|
|
8
|
+
import type { EditorErrorIssue, EditorErrorState } from './entityEditorErrors';
|
|
9
|
+
import type { EntityEditorRenderExtension } from './extensions';
|
|
10
|
+
import NgbEntityAuditSidebar from './NgbEntityAuditSidebar.vue';
|
|
11
|
+
import NgbEditorDiscardDialog from './NgbEditorDiscardDialog.vue';
|
|
12
|
+
import NgbEntityEditorHeader from './NgbEntityEditorHeader.vue';
|
|
13
|
+
import type {
|
|
14
|
+
DocumentHeaderActionGroup,
|
|
15
|
+
DocumentHeaderActionItem,
|
|
16
|
+
EditorKind,
|
|
17
|
+
EditorMode,
|
|
18
|
+
EntityHeaderIconAction,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
22
|
+
kind: EditorKind;
|
|
23
|
+
mode: EditorMode;
|
|
24
|
+
canBack?: boolean;
|
|
25
|
+
title: string;
|
|
26
|
+
subtitle?: string;
|
|
27
|
+
documentStatusLabel?: string;
|
|
28
|
+
documentStatusTone?: 'neutral' | 'success' | 'warn';
|
|
29
|
+
loading: boolean;
|
|
30
|
+
saving: boolean;
|
|
31
|
+
pageActions?: EntityHeaderIconAction[];
|
|
32
|
+
documentPrimaryActions?: DocumentHeaderActionItem[];
|
|
33
|
+
documentMoreActionGroups?: DocumentHeaderActionGroup[];
|
|
34
|
+
isNew: boolean;
|
|
35
|
+
isMarkedForDeletion: boolean;
|
|
36
|
+
displayedError?: EditorErrorState | null;
|
|
37
|
+
bannerIssues?: EditorErrorIssue[];
|
|
38
|
+
form?: FormMetadata | null;
|
|
39
|
+
model: EntityFormModel;
|
|
40
|
+
entityTypeCode: string;
|
|
41
|
+
status?: DocumentStatusValue;
|
|
42
|
+
isReadOnly?: boolean;
|
|
43
|
+
errors?: Record<string, string | string[] | null> | null;
|
|
44
|
+
afterFormExtensions?: EntityEditorRenderExtension[];
|
|
45
|
+
dialogExtensions?: EntityEditorRenderExtension[];
|
|
46
|
+
auditOpen?: boolean;
|
|
47
|
+
auditEntityKind?: number;
|
|
48
|
+
auditEntityId?: string | null;
|
|
49
|
+
auditEntityTitle?: string;
|
|
50
|
+
leaveOpen?: boolean;
|
|
51
|
+
markConfirmOpen?: boolean;
|
|
52
|
+
markConfirmMessage?: string;
|
|
53
|
+
}>(), {
|
|
54
|
+
canBack: true,
|
|
55
|
+
subtitle: undefined,
|
|
56
|
+
documentStatusLabel: 'Draft',
|
|
57
|
+
documentStatusTone: 'neutral',
|
|
58
|
+
pageActions: () => [],
|
|
59
|
+
documentPrimaryActions: () => [],
|
|
60
|
+
documentMoreActionGroups: () => [],
|
|
61
|
+
displayedError: null,
|
|
62
|
+
bannerIssues: () => [],
|
|
63
|
+
form: null,
|
|
64
|
+
status: undefined,
|
|
65
|
+
isReadOnly: false,
|
|
66
|
+
errors: null,
|
|
67
|
+
afterFormExtensions: () => [],
|
|
68
|
+
dialogExtensions: () => [],
|
|
69
|
+
auditOpen: false,
|
|
70
|
+
auditEntityKind: 0,
|
|
71
|
+
auditEntityId: null,
|
|
72
|
+
auditEntityTitle: '',
|
|
73
|
+
leaveOpen: false,
|
|
74
|
+
markConfirmOpen: false,
|
|
75
|
+
markConfirmMessage: '',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{
|
|
79
|
+
(e: 'back'): void;
|
|
80
|
+
(e: 'close'): void;
|
|
81
|
+
(e: 'action', action: string): void;
|
|
82
|
+
(e: 'closeAuditLog'): void;
|
|
83
|
+
(e: 'cancelLeave'): void;
|
|
84
|
+
(e: 'confirmLeave'): void;
|
|
85
|
+
(e: 'cancelMarkForDeletion'): void;
|
|
86
|
+
(e: 'confirmMarkForDeletion'): void;
|
|
87
|
+
}>();
|
|
88
|
+
|
|
89
|
+
const formRef = ref<InstanceType<typeof NgbEntityForm> | null>(null);
|
|
90
|
+
|
|
91
|
+
const visibleBannerIssues = computed(() => {
|
|
92
|
+
const summary = normalizeBannerText(props.displayedError?.summary);
|
|
93
|
+
|
|
94
|
+
return props.bannerIssues.filter((issue) => {
|
|
95
|
+
if (issue.path !== '_form') return true;
|
|
96
|
+
return normalizeBannerText(issue.messages.join(' ')) !== summary;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function focusField(path: string): boolean {
|
|
101
|
+
return !!formRef.value?.focusField?.(path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function focusFirstError(keys: string[]): boolean {
|
|
105
|
+
return !!formRef.value?.focusFirstError?.(keys);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
defineExpose({
|
|
109
|
+
focusField,
|
|
110
|
+
focusFirstError,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function normalizeBannerText(value: string | null | undefined): string {
|
|
114
|
+
return String(value ?? '')
|
|
115
|
+
.trim()
|
|
116
|
+
.replace(/\s+/g, ' ')
|
|
117
|
+
.toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<div :class="mode === 'page' ? 'flex h-full min-h-0 flex-col' : 'flex flex-col'">
|
|
123
|
+
<NgbEntityEditorHeader
|
|
124
|
+
:kind="kind"
|
|
125
|
+
:mode="mode"
|
|
126
|
+
:can-back="canBack"
|
|
127
|
+
:title="title"
|
|
128
|
+
:subtitle="subtitle"
|
|
129
|
+
:document-status-label="documentStatusLabel"
|
|
130
|
+
:document-status-tone="documentStatusTone"
|
|
131
|
+
:loading="loading"
|
|
132
|
+
:saving="saving"
|
|
133
|
+
:page-actions="pageActions"
|
|
134
|
+
:document-primary-actions="documentPrimaryActions"
|
|
135
|
+
:document-more-action-groups="documentMoreActionGroups"
|
|
136
|
+
@back="emit('back')"
|
|
137
|
+
@close="emit('close')"
|
|
138
|
+
@action="(action) => emit('action', action)"
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<div :class="mode === 'page' ? 'min-h-0 flex-1 overflow-auto p-6' : 'px-5 py-4'">
|
|
142
|
+
<div
|
|
143
|
+
v-if="!isNew && isMarkedForDeletion"
|
|
144
|
+
class="mb-4 flex items-start justify-between gap-3 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 dark:border-red-900/50 dark:bg-red-950/30"
|
|
145
|
+
>
|
|
146
|
+
<div class="min-w-0">
|
|
147
|
+
<div class="text-sm font-semibold text-ngb-danger">Deleted</div>
|
|
148
|
+
<div class="mt-1 text-sm text-ngb-muted">
|
|
149
|
+
This {{ kind === 'catalog' ? 'record' : 'document' }} is marked for deletion. Restore it to edit or post.
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div
|
|
155
|
+
v-if="displayedError"
|
|
156
|
+
class="mb-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100"
|
|
157
|
+
>
|
|
158
|
+
<div class="font-medium leading-6">{{ displayedError.summary }}</div>
|
|
159
|
+
<ul v-if="visibleBannerIssues.length" class="mt-2 list-disc space-y-1 pl-5">
|
|
160
|
+
<li v-for="issue in visibleBannerIssues" :key="`${issue.scope}:${issue.path}:${issue.messages.join('|')}`">
|
|
161
|
+
<template v-if="issue.path === '_form'">
|
|
162
|
+
<span class="font-medium">{{ issue.messages.join(' ') }}</span>
|
|
163
|
+
</template>
|
|
164
|
+
<template v-else>
|
|
165
|
+
<span class="font-medium">{{ issue.label }}:</span>
|
|
166
|
+
<span> {{ issue.messages.join(' ') }}</span>
|
|
167
|
+
</template>
|
|
168
|
+
</li>
|
|
169
|
+
</ul>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div v-if="loading" class="text-sm text-ngb-muted">Loading…</div>
|
|
173
|
+
|
|
174
|
+
<div v-else class="space-y-4">
|
|
175
|
+
<template v-if="form">
|
|
176
|
+
<NgbEntityForm
|
|
177
|
+
ref="formRef"
|
|
178
|
+
:form="form"
|
|
179
|
+
:model="model"
|
|
180
|
+
:entity-type-code="entityTypeCode"
|
|
181
|
+
:status="kind === 'document' ? status : undefined"
|
|
182
|
+
:force-readonly="isReadOnly"
|
|
183
|
+
:presentation="mode === 'drawer' ? 'flat' : 'sections'"
|
|
184
|
+
:errors="errors"
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
<component
|
|
188
|
+
:is="extension.component"
|
|
189
|
+
v-for="extension in afterFormExtensions"
|
|
190
|
+
:key="extension.key"
|
|
191
|
+
:ref="extension.componentRef ?? undefined"
|
|
192
|
+
v-bind="extension.props"
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<slot name="after-form" />
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<div v-else class="text-sm text-ngb-muted">No form metadata available.</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<component
|
|
203
|
+
:is="extension.component"
|
|
204
|
+
v-for="extension in dialogExtensions"
|
|
205
|
+
:key="extension.key"
|
|
206
|
+
:ref="extension.componentRef ?? undefined"
|
|
207
|
+
v-bind="extension.props"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<slot name="dialogs" />
|
|
211
|
+
|
|
212
|
+
<NgbDrawer
|
|
213
|
+
:open="auditOpen"
|
|
214
|
+
title="Audit Log"
|
|
215
|
+
hide-header
|
|
216
|
+
flush-body
|
|
217
|
+
@update:open="(value) => (!value ? emit('closeAuditLog') : null)"
|
|
218
|
+
>
|
|
219
|
+
<NgbEntityAuditSidebar
|
|
220
|
+
:open="auditOpen"
|
|
221
|
+
:entity-kind="auditEntityKind"
|
|
222
|
+
:entity-id="auditEntityId"
|
|
223
|
+
:entity-title="auditEntityTitle"
|
|
224
|
+
@back="emit('closeAuditLog')"
|
|
225
|
+
@close="emit('closeAuditLog')"
|
|
226
|
+
/>
|
|
227
|
+
</NgbDrawer>
|
|
228
|
+
|
|
229
|
+
<NgbEditorDiscardDialog
|
|
230
|
+
:open="leaveOpen"
|
|
231
|
+
title="Discard changes?"
|
|
232
|
+
message="You have unsaved changes. If you leave now, they won’t be saved."
|
|
233
|
+
confirm-text="Leave"
|
|
234
|
+
cancel-text="Stay"
|
|
235
|
+
@cancel="emit('cancelLeave')"
|
|
236
|
+
@confirm="emit('confirmLeave')"
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
<NgbConfirmDialog
|
|
240
|
+
:open="markConfirmOpen"
|
|
241
|
+
title="Mark for deletion?"
|
|
242
|
+
:message="markConfirmMessage"
|
|
243
|
+
confirm-text="Mark"
|
|
244
|
+
cancel-text="Cancel"
|
|
245
|
+
danger
|
|
246
|
+
@update:open="(value) => (!value ? emit('cancelMarkForDeletion') : null)"
|
|
247
|
+
@confirm="emit('confirmMarkForDeletion')"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
import NgbIcon from '../primitives/NgbIcon.vue';
|
|
5
|
+
|
|
6
|
+
import type { EntityEditorFlags, EntityHeaderIconAction } from './types';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<{
|
|
9
|
+
flags: EntityEditorFlags;
|
|
10
|
+
extraActions?: EntityHeaderIconAction[];
|
|
11
|
+
restoreTitle?: string;
|
|
12
|
+
markForDeletionTitle?: string;
|
|
13
|
+
}>(), {
|
|
14
|
+
extraActions: () => [],
|
|
15
|
+
restoreTitle: 'Unmark for deletion',
|
|
16
|
+
markForDeletionTitle: 'Mark for deletion',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'action', action: string): void;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const actionDisabled = computed(() => props.flags.loading || props.flags.saving);
|
|
24
|
+
const markTitle = computed(() => props.flags.canUnmarkForDeletion ? props.restoreTitle : props.markForDeletionTitle);
|
|
25
|
+
const markIcon = computed(() => props.flags.canUnmarkForDeletion ? 'trash-restore' : 'trash');
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<button
|
|
30
|
+
v-if="flags.canExpand"
|
|
31
|
+
class="ngb-iconbtn"
|
|
32
|
+
:disabled="actionDisabled"
|
|
33
|
+
title="Open full page"
|
|
34
|
+
@click="emit('action', 'expand')"
|
|
35
|
+
>
|
|
36
|
+
<NgbIcon name="open-in-new" />
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<button
|
|
40
|
+
v-if="flags.canShareLink"
|
|
41
|
+
class="ngb-iconbtn"
|
|
42
|
+
:disabled="actionDisabled"
|
|
43
|
+
title="Share link"
|
|
44
|
+
@click="emit('action', 'share')"
|
|
45
|
+
>
|
|
46
|
+
<NgbIcon name="share" />
|
|
47
|
+
</button>
|
|
48
|
+
|
|
49
|
+
<button
|
|
50
|
+
v-if="flags.canShowAudit"
|
|
51
|
+
class="ngb-iconbtn"
|
|
52
|
+
:disabled="actionDisabled"
|
|
53
|
+
title="Audit log"
|
|
54
|
+
@click="emit('action', 'audit')"
|
|
55
|
+
>
|
|
56
|
+
<NgbIcon name="history" />
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
<button
|
|
60
|
+
v-if="flags.canMarkForDeletion || flags.canUnmarkForDeletion"
|
|
61
|
+
class="ngb-iconbtn"
|
|
62
|
+
:disabled="actionDisabled"
|
|
63
|
+
:title="markTitle"
|
|
64
|
+
@click="emit('action', 'mark')"
|
|
65
|
+
>
|
|
66
|
+
<NgbIcon :name="markIcon" />
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<button
|
|
70
|
+
v-for="item in extraActions"
|
|
71
|
+
:key="item.key"
|
|
72
|
+
class="ngb-iconbtn"
|
|
73
|
+
:disabled="item.disabled"
|
|
74
|
+
:title="item.title"
|
|
75
|
+
@click="emit('action', item.key)"
|
|
76
|
+
>
|
|
77
|
+
<NgbIcon :name="item.icon" />
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
<button
|
|
81
|
+
class="ngb-iconbtn"
|
|
82
|
+
:disabled="actionDisabled || !flags.canSave"
|
|
83
|
+
title="Save"
|
|
84
|
+
@click="emit('action', 'save')"
|
|
85
|
+
>
|
|
86
|
+
<NgbIcon name="save" />
|
|
87
|
+
</button>
|
|
88
|
+
</template>
|