@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,115 @@
|
|
|
1
|
+
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
|
2
|
+
|
|
3
|
+
import type { DocumentTypeMetadata, FormMetadata } from '../metadata/types';
|
|
4
|
+
import {
|
|
5
|
+
currentRouteBackTarget,
|
|
6
|
+
resolveBackTarget,
|
|
7
|
+
resolveBackTargetFromPath,
|
|
8
|
+
routeTargetMatches,
|
|
9
|
+
withBackTarget,
|
|
10
|
+
} from '../router/backNavigation';
|
|
11
|
+
import { resolveNgbEditorRouting } from './config';
|
|
12
|
+
import type { EditorKind, EditorMode } from './types';
|
|
13
|
+
|
|
14
|
+
type FormFieldDescriptor = {
|
|
15
|
+
key?: string;
|
|
16
|
+
isReadOnly?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type FormShape = {
|
|
20
|
+
sections?: Array<{
|
|
21
|
+
rows?: Array<{
|
|
22
|
+
fields?: FormFieldDescriptor[];
|
|
23
|
+
}>;
|
|
24
|
+
}>;
|
|
25
|
+
} | null | undefined;
|
|
26
|
+
|
|
27
|
+
export function documentHasTables(meta: DocumentTypeMetadata | null | undefined): boolean {
|
|
28
|
+
return Array.isArray(meta?.parts) && meta.parts.length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function shouldOpenDocumentInFullPageByDefault(meta: DocumentTypeMetadata | null | undefined): boolean {
|
|
32
|
+
return documentHasTables(meta);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildDocumentFullPageUrl(documentType: string, id?: string | null): string {
|
|
36
|
+
return resolveNgbEditorRouting().buildDocumentFullPageUrl(documentType, id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildDocumentCompactPageUrl(documentType: string, id?: string | null): string {
|
|
40
|
+
return resolveNgbEditorRouting().buildDocumentCompactPageUrl(documentType, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildDocumentEffectsPageUrl(documentType: string, id: string): string {
|
|
44
|
+
return resolveNgbEditorRouting().buildDocumentEffectsPageUrl(documentType, id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildDocumentFlowPageUrl(documentType: string, id: string): string {
|
|
48
|
+
return resolveNgbEditorRouting().buildDocumentFlowPageUrl(documentType, id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildDocumentPrintPageUrl(
|
|
52
|
+
documentType: string,
|
|
53
|
+
id: string,
|
|
54
|
+
options?: { autoPrint?: boolean },
|
|
55
|
+
): string {
|
|
56
|
+
return resolveNgbEditorRouting().buildDocumentPrintPageUrl(documentType, id, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveCompactDocumentSourceTarget(
|
|
60
|
+
route: Pick<RouteLocationNormalizedLoaded, 'query'>,
|
|
61
|
+
compactTarget: string | null | undefined,
|
|
62
|
+
): string | null {
|
|
63
|
+
const explicitBack = resolveBackTarget(route);
|
|
64
|
+
if (routeTargetMatches(explicitBack, compactTarget)) return explicitBack;
|
|
65
|
+
|
|
66
|
+
const nestedBack = resolveBackTargetFromPath(explicitBack);
|
|
67
|
+
if (routeTargetMatches(nestedBack, compactTarget)) return nestedBack;
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveDocumentReopenTarget(
|
|
73
|
+
route: Pick<RouteLocationNormalizedLoaded, 'query' | 'fullPath'>,
|
|
74
|
+
documentType: string,
|
|
75
|
+
id: string,
|
|
76
|
+
compactTarget?: string | null,
|
|
77
|
+
): string {
|
|
78
|
+
const compactSourceTarget = resolveCompactDocumentSourceTarget(
|
|
79
|
+
route,
|
|
80
|
+
compactTarget ?? buildDocumentCompactPageUrl(documentType, id),
|
|
81
|
+
);
|
|
82
|
+
if (compactSourceTarget) return compactSourceTarget;
|
|
83
|
+
|
|
84
|
+
const explicitBack = resolveBackTarget(route);
|
|
85
|
+
if (explicitBack) return explicitBack;
|
|
86
|
+
|
|
87
|
+
return withBackTarget(
|
|
88
|
+
buildDocumentFullPageUrl(documentType, id),
|
|
89
|
+
currentRouteBackTarget(route),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildEntityFallbackCloseTarget(kind: EditorKind, typeCode: string): string {
|
|
94
|
+
return `${kind === 'catalog' ? '/catalogs/' : '/documents/'}${typeCode}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveNavigateOnCreate(value: boolean | undefined, mode: EditorMode): boolean {
|
|
98
|
+
return value ?? mode === 'page';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function listFormFields(form: FormShape): FormFieldDescriptor[] {
|
|
102
|
+
const out: FormFieldDescriptor[] = [];
|
|
103
|
+
for (const section of form?.sections ?? []) {
|
|
104
|
+
for (const row of section.rows ?? []) {
|
|
105
|
+
for (const field of row.fields ?? []) out.push(field);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formMetadataFieldKeys(form: FormMetadata | null | undefined): string[] {
|
|
112
|
+
return listFormFields(form)
|
|
113
|
+
.map((field) => String(field?.key ?? '').trim())
|
|
114
|
+
.filter((key) => key.length > 0);
|
|
115
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DocumentStatusValue } from '../metadata/types';
|
|
2
|
+
|
|
3
|
+
export type DocumentStatusTone = 'neutral' | 'success' | 'warn';
|
|
4
|
+
export type DocumentStatusVisual = 'saved' | 'posted' | 'marked';
|
|
5
|
+
|
|
6
|
+
export function normalizeDocumentStatusValue(value: unknown): DocumentStatusValue {
|
|
7
|
+
if (typeof value === 'number' && Number.isFinite(value) && (value === 1 || value === 2 || value === 3)) {
|
|
8
|
+
return value as DocumentStatusValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
12
|
+
if (normalized === '1' || normalized === 'draft') return 1;
|
|
13
|
+
if (normalized === '2' || normalized === 'posted') return 2;
|
|
14
|
+
if (
|
|
15
|
+
normalized === '3'
|
|
16
|
+
|| normalized === 'deleted'
|
|
17
|
+
|| normalized === 'markedfordeletion'
|
|
18
|
+
|| normalized === 'marked_for_deletion'
|
|
19
|
+
|| normalized === 'marked-for-deletion'
|
|
20
|
+
|| normalized === 'marked for deletion'
|
|
21
|
+
) return 3;
|
|
22
|
+
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function documentStatusLabel(value: unknown): string {
|
|
27
|
+
const status = normalizeDocumentStatusValue(value);
|
|
28
|
+
if (status === 2) return 'Posted';
|
|
29
|
+
if (status === 3) return 'Deleted';
|
|
30
|
+
return 'Draft';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function documentStatusTone(value: unknown): DocumentStatusTone {
|
|
34
|
+
const status = normalizeDocumentStatusValue(value);
|
|
35
|
+
if (status === 2) return 'success';
|
|
36
|
+
if (status === 3) return 'warn';
|
|
37
|
+
return 'neutral';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function documentStatusVisual(value: unknown): DocumentStatusVisual {
|
|
41
|
+
const status = normalizeDocumentStatusValue(value);
|
|
42
|
+
if (status === 2) return 'posted';
|
|
43
|
+
if (status === 3) return 'marked';
|
|
44
|
+
return 'saved';
|
|
45
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ApiError, type ApiValidationIssue } from '../api/http';
|
|
2
|
+
|
|
3
|
+
export const ENTITY_EDITOR_FORM_ISSUE_PATH = '_form';
|
|
4
|
+
|
|
5
|
+
export type EditorErrorIssue = {
|
|
6
|
+
path: string;
|
|
7
|
+
label: string;
|
|
8
|
+
scope: string;
|
|
9
|
+
messages: string[];
|
|
10
|
+
code?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EditorErrorState = {
|
|
14
|
+
summary: string;
|
|
15
|
+
issues: EditorErrorIssue[];
|
|
16
|
+
errorCode?: string | null;
|
|
17
|
+
status?: number | null;
|
|
18
|
+
context?: Record<string, unknown> | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NormalizeEntityEditorErrorOptions = {
|
|
22
|
+
resolveIssueLabel?: (path: string) => string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function dedupeEntityEditorMessages(messages: string[]): string[] {
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
const result: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
const value = String(message ?? '').trim();
|
|
31
|
+
if (!value) continue;
|
|
32
|
+
|
|
33
|
+
const key = value.toLowerCase();
|
|
34
|
+
if (seen.has(key)) continue;
|
|
35
|
+
seen.add(key);
|
|
36
|
+
result.push(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function humanizeEntityEditorFieldKey(key: string): string {
|
|
43
|
+
const base = String(key ?? '').trim();
|
|
44
|
+
if (!base) return 'Field';
|
|
45
|
+
|
|
46
|
+
const normalized = base
|
|
47
|
+
.replace(/_utc$/i, '')
|
|
48
|
+
.replace(/_id$/i, '')
|
|
49
|
+
.replace(/[._]/g, ' ')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
|
|
53
|
+
const words = normalized.split(' ').filter(Boolean).map((word) => {
|
|
54
|
+
const lower = word.toLowerCase();
|
|
55
|
+
if (lower === 'no') return 'No';
|
|
56
|
+
if (lower === 'id') return 'ID';
|
|
57
|
+
if (/^line\d+$/i.test(word)) return `Line ${word.slice(4)}`;
|
|
58
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return words.join(' ') || 'Field';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isEntityEditorFormIssuePath(path: string): boolean {
|
|
65
|
+
const raw = String(path ?? '').trim();
|
|
66
|
+
return raw.length === 0 || raw === ENTITY_EDITOR_FORM_ISSUE_PATH;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultEditorIssueLabel(path: string): string {
|
|
70
|
+
if (isEntityEditorFormIssuePath(path)) return 'Validation';
|
|
71
|
+
return humanizeEntityEditorFieldKey(path);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildEditorIssuesFromApiIssues(
|
|
75
|
+
issues: ApiValidationIssue[],
|
|
76
|
+
resolveIssueLabel: (path: string) => string,
|
|
77
|
+
): EditorErrorIssue[] {
|
|
78
|
+
const buckets = new Map<string, EditorErrorIssue>();
|
|
79
|
+
|
|
80
|
+
for (const issue of issues) {
|
|
81
|
+
const path = String(issue.path ?? '').trim() || ENTITY_EDITOR_FORM_ISSUE_PATH;
|
|
82
|
+
const scope = String(issue.scope ?? '').trim() || (isEntityEditorFormIssuePath(path) ? 'form' : 'field');
|
|
83
|
+
const message = String(issue.message ?? '').trim();
|
|
84
|
+
if (!message) continue;
|
|
85
|
+
|
|
86
|
+
const bucketKey = `${scope}:${path}`;
|
|
87
|
+
const current = buckets.get(bucketKey) ?? {
|
|
88
|
+
path,
|
|
89
|
+
label: resolveIssueLabel(path),
|
|
90
|
+
scope,
|
|
91
|
+
messages: [],
|
|
92
|
+
code: issue.code ?? null,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
current.messages = dedupeEntityEditorMessages([...current.messages, message]);
|
|
96
|
+
if (!current.code && issue.code) current.code = issue.code;
|
|
97
|
+
buckets.set(bucketKey, current);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Array.from(buckets.values());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildEditorIssuesFromLegacyErrors(
|
|
104
|
+
rawErrors: Record<string, string[] | string>,
|
|
105
|
+
resolveIssueLabel: (path: string) => string,
|
|
106
|
+
): EditorErrorIssue[] {
|
|
107
|
+
const issues: EditorErrorIssue[] = [];
|
|
108
|
+
|
|
109
|
+
for (const [path, values] of Object.entries(rawErrors)) {
|
|
110
|
+
const messages = dedupeEntityEditorMessages(Array.isArray(values) ? values : [String(values)]);
|
|
111
|
+
if (messages.length === 0) continue;
|
|
112
|
+
|
|
113
|
+
issues.push({
|
|
114
|
+
path,
|
|
115
|
+
label: resolveIssueLabel(path),
|
|
116
|
+
scope: isEntityEditorFormIssuePath(path) ? 'form' : 'field',
|
|
117
|
+
messages,
|
|
118
|
+
code: null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return issues;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveEntityEditorErrorSummary(summary: string, issues: EditorErrorIssue[]): string {
|
|
126
|
+
const fallback = summary.trim() || 'Request failed.';
|
|
127
|
+
const hasHighlightableIssues = issues.some((issue) => !isEntityEditorFormIssuePath(issue.path));
|
|
128
|
+
if (hasHighlightableIssues) return 'Please fix the highlighted fields.';
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function normalizeEntityEditorError(
|
|
133
|
+
cause: unknown,
|
|
134
|
+
options: NormalizeEntityEditorErrorOptions = {},
|
|
135
|
+
): EditorErrorState {
|
|
136
|
+
const resolveIssueLabel = options.resolveIssueLabel ?? defaultEditorIssueLabel;
|
|
137
|
+
|
|
138
|
+
if (cause instanceof ApiError) {
|
|
139
|
+
const issues = cause.issues && cause.issues.length > 0
|
|
140
|
+
? buildEditorIssuesFromApiIssues(cause.issues, resolveIssueLabel)
|
|
141
|
+
: cause.errors
|
|
142
|
+
? buildEditorIssuesFromLegacyErrors(cause.errors, resolveIssueLabel)
|
|
143
|
+
: [];
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
summary: resolveEntityEditorErrorSummary(String(cause.message ?? '').trim(), issues),
|
|
147
|
+
issues,
|
|
148
|
+
errorCode: cause.errorCode ?? null,
|
|
149
|
+
status: cause.status,
|
|
150
|
+
context: cause.context ?? null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const summary = cause instanceof Error ? cause.message : String(cause ?? 'Request failed.');
|
|
155
|
+
return {
|
|
156
|
+
summary: summary.trim() || 'Request failed.',
|
|
157
|
+
issues: [],
|
|
158
|
+
errorCode: null,
|
|
159
|
+
status: null,
|
|
160
|
+
context: null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { EntityFormModel, RecordPayload } from '../metadata/types';
|
|
4
|
+
import type { ToastApi as SharedToastApi } from '../primitives/toast';
|
|
5
|
+
import { clonePlainData } from '../utils/clone';
|
|
6
|
+
import type { EditorChangeReason, EditorKind } from './types';
|
|
7
|
+
import type { EditorErrorState } from './entityEditorErrors';
|
|
8
|
+
|
|
9
|
+
type EntityEditorMetadataWithForm = {
|
|
10
|
+
form?: unknown | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EntityEditorToastApi = Pick<SharedToastApi, 'push'>;
|
|
14
|
+
|
|
15
|
+
export type EntityEditorMetadataStoreLike<TCatalogMeta, TDocumentMeta> = {
|
|
16
|
+
ensureCatalogType: (typeCode: string) => Promise<TCatalogMeta>;
|
|
17
|
+
ensureDocumentType: (typeCode: string) => Promise<TDocumentMeta>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CatalogEntityPersistenceAdapter = {
|
|
21
|
+
load: () => Promise<void>;
|
|
22
|
+
save: () => Promise<void>;
|
|
23
|
+
markForDeletion: () => Promise<void>;
|
|
24
|
+
unmarkForDeletion: () => Promise<void>;
|
|
25
|
+
deleteEntity: () => Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type DocumentEntityPersistenceAdapter = {
|
|
29
|
+
load: () => Promise<void>;
|
|
30
|
+
save: () => Promise<void>;
|
|
31
|
+
markForDeletion: () => Promise<void>;
|
|
32
|
+
unmarkForDeletion: () => Promise<void>;
|
|
33
|
+
post: () => Promise<void>;
|
|
34
|
+
unpost: () => Promise<void>;
|
|
35
|
+
loadEffectsSnapshot?: (documentType: string, id: string) => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type UseEntityEditorPersistenceArgs<
|
|
39
|
+
TMetadata extends EntityEditorMetadataWithForm = EntityEditorMetadataWithForm,
|
|
40
|
+
> = {
|
|
41
|
+
kind: ComputedRef<EditorKind>;
|
|
42
|
+
typeCode: ComputedRef<string>;
|
|
43
|
+
metadata: ComputedRef<TMetadata | null>;
|
|
44
|
+
loading: Ref<boolean>;
|
|
45
|
+
saving: Ref<boolean>;
|
|
46
|
+
canSave: ComputedRef<boolean>;
|
|
47
|
+
canMarkForDeletion: ComputedRef<boolean>;
|
|
48
|
+
canUnmarkForDeletion: ComputedRef<boolean>;
|
|
49
|
+
canDelete: ComputedRef<boolean>;
|
|
50
|
+
canPost: ComputedRef<boolean>;
|
|
51
|
+
canUnpost: ComputedRef<boolean>;
|
|
52
|
+
isNew: ComputedRef<boolean>;
|
|
53
|
+
isDirty: ComputedRef<boolean>;
|
|
54
|
+
error: Ref<EditorErrorState | null>;
|
|
55
|
+
setEditorError: (value: EditorErrorState | null) => void;
|
|
56
|
+
normalizeEditorError: (cause: unknown) => EditorErrorState;
|
|
57
|
+
emitChanged: (reason?: EditorChangeReason) => void;
|
|
58
|
+
emitDeleted: () => void;
|
|
59
|
+
adapters: {
|
|
60
|
+
catalog: CatalogEntityPersistenceAdapter;
|
|
61
|
+
document: DocumentEntityPersistenceAdapter;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function applyInitialFieldValues(target: EntityFormModel, source: EntityFormModel | null) {
|
|
66
|
+
if (!source) return;
|
|
67
|
+
|
|
68
|
+
for (const [key, value] of Object.entries(source)) {
|
|
69
|
+
if (value === undefined) continue;
|
|
70
|
+
target[key] = clonePlainData(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setModelFromFields(
|
|
75
|
+
target: Ref<EntityFormModel>,
|
|
76
|
+
fields: RecordPayload['fields'] | null | undefined,
|
|
77
|
+
) {
|
|
78
|
+
target.value = { ...((fields ?? {}) as EntityFormModel) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useEntityEditorPersistence<
|
|
82
|
+
TMetadata extends EntityEditorMetadataWithForm = EntityEditorMetadataWithForm,
|
|
83
|
+
>(args: UseEntityEditorPersistenceArgs<TMetadata>) {
|
|
84
|
+
async function load() {
|
|
85
|
+
if (!args.typeCode.value) return;
|
|
86
|
+
|
|
87
|
+
args.loading.value = true;
|
|
88
|
+
args.setEditorError(null);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (args.kind.value === 'catalog') {
|
|
92
|
+
await args.adapters.catalog.load();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await args.adapters.document.load();
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
99
|
+
} finally {
|
|
100
|
+
args.loading.value = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function save() {
|
|
105
|
+
if (!args.metadata.value?.form) return;
|
|
106
|
+
if (!args.canSave.value) return;
|
|
107
|
+
|
|
108
|
+
args.saving.value = true;
|
|
109
|
+
args.setEditorError(null);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (args.kind.value === 'catalog') {
|
|
113
|
+
await args.adapters.catalog.save();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await args.adapters.document.save();
|
|
118
|
+
} catch (cause) {
|
|
119
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
120
|
+
} finally {
|
|
121
|
+
args.saving.value = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function markForDeletion() {
|
|
126
|
+
if (args.isNew.value || !args.canMarkForDeletion.value) return;
|
|
127
|
+
|
|
128
|
+
args.saving.value = true;
|
|
129
|
+
args.setEditorError(null);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
if (args.kind.value === 'catalog') {
|
|
133
|
+
await args.adapters.catalog.markForDeletion();
|
|
134
|
+
} else {
|
|
135
|
+
await args.adapters.document.markForDeletion();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await load();
|
|
139
|
+
args.emitChanged('markForDeletion');
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
142
|
+
} finally {
|
|
143
|
+
args.saving.value = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function unmarkForDeletion() {
|
|
148
|
+
if (args.isNew.value || !args.canUnmarkForDeletion.value) return;
|
|
149
|
+
|
|
150
|
+
args.saving.value = true;
|
|
151
|
+
args.setEditorError(null);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (args.kind.value === 'catalog') {
|
|
155
|
+
await args.adapters.catalog.unmarkForDeletion();
|
|
156
|
+
} else {
|
|
157
|
+
await args.adapters.document.unmarkForDeletion();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await load();
|
|
161
|
+
args.emitChanged('unmarkForDeletion');
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
164
|
+
} finally {
|
|
165
|
+
args.saving.value = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function deleteEntity() {
|
|
170
|
+
if (args.kind.value !== 'catalog' || args.isNew.value || !args.canDelete.value) return;
|
|
171
|
+
|
|
172
|
+
args.saving.value = true;
|
|
173
|
+
args.setEditorError(null);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await args.adapters.catalog.deleteEntity();
|
|
177
|
+
await load();
|
|
178
|
+
args.emitDeleted();
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
181
|
+
} finally {
|
|
182
|
+
args.saving.value = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function post() {
|
|
187
|
+
if (!args.canPost.value) return;
|
|
188
|
+
|
|
189
|
+
args.setEditorError(null);
|
|
190
|
+
|
|
191
|
+
if (args.isDirty.value) {
|
|
192
|
+
await save();
|
|
193
|
+
if (args.error.value) return;
|
|
194
|
+
if (!args.canPost.value) return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
args.saving.value = true;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await args.adapters.document.post();
|
|
201
|
+
await load();
|
|
202
|
+
args.emitChanged('post');
|
|
203
|
+
} catch (cause) {
|
|
204
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
205
|
+
} finally {
|
|
206
|
+
args.saving.value = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function unpost() {
|
|
211
|
+
if (!args.canUnpost.value) return;
|
|
212
|
+
|
|
213
|
+
args.saving.value = true;
|
|
214
|
+
args.setEditorError(null);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await args.adapters.document.unpost();
|
|
218
|
+
await load();
|
|
219
|
+
args.emitChanged('unpost');
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
args.setEditorError(args.normalizeEditorError(cause));
|
|
222
|
+
} finally {
|
|
223
|
+
args.saving.value = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function loadDocumentEffectsSnapshot(documentType: string, id: string) {
|
|
228
|
+
await args.adapters.document.loadEffectsSnapshot?.(documentType, id);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
load,
|
|
233
|
+
save,
|
|
234
|
+
markForDeletion,
|
|
235
|
+
unmarkForDeletion,
|
|
236
|
+
deleteEntity,
|
|
237
|
+
post,
|
|
238
|
+
unpost,
|
|
239
|
+
loadDocumentEffectsSnapshot,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Component, ComputedRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { Awaitable } from '../metadata/types';
|
|
4
|
+
import type { EditorKind, EditorMode, EntityHeaderIconAction } from './types';
|
|
5
|
+
|
|
6
|
+
export type EntityEditorRenderExtension = {
|
|
7
|
+
key: string;
|
|
8
|
+
component: Component;
|
|
9
|
+
props?: Record<string, unknown>;
|
|
10
|
+
componentRef?: ((value: unknown) => void) | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type EntityEditorActionHandler = () => Awaitable<void>;
|
|
14
|
+
export type EntityEditorActionHandlerMap = Record<string, EntityEditorActionHandler | undefined>;
|
|
15
|
+
|
|
16
|
+
export type UseEntityEditorPageActionsArgs = {
|
|
17
|
+
kind: ComputedRef<EditorKind>;
|
|
18
|
+
mode: ComputedRef<EditorMode>;
|
|
19
|
+
compactTo: ComputedRef<string | null>;
|
|
20
|
+
loading: ComputedRef<boolean>;
|
|
21
|
+
saving: ComputedRef<boolean>;
|
|
22
|
+
isNew: ComputedRef<boolean>;
|
|
23
|
+
isMarkedForDeletion: ComputedRef<boolean>;
|
|
24
|
+
canSave: ComputedRef<boolean>;
|
|
25
|
+
canShareLink: ComputedRef<boolean>;
|
|
26
|
+
canOpenAudit: ComputedRef<boolean>;
|
|
27
|
+
canMarkForDeletion: ComputedRef<boolean>;
|
|
28
|
+
canUnmarkForDeletion: ComputedRef<boolean>;
|
|
29
|
+
extraActions?: ComputedRef<EntityHeaderIconAction[]>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function runEntityEditorAction(
|
|
33
|
+
action: string,
|
|
34
|
+
handlers: EntityEditorActionHandlerMap,
|
|
35
|
+
): boolean {
|
|
36
|
+
const handler = handlers[action];
|
|
37
|
+
if (!handler) return false;
|
|
38
|
+
void Promise.resolve(handler());
|
|
39
|
+
return true;
|
|
40
|
+
}
|