@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,120 @@
|
|
|
1
|
+
import { buildChartOfAccountsPath } from '../accounting/navigation'
|
|
2
|
+
import { buildCatalogFullPageUrl } from '../editor/catalogNavigation'
|
|
3
|
+
import { buildDocumentFullPageUrl } from '../editor/documentNavigation'
|
|
4
|
+
import type { Awaitable, LookupSource } from '../metadata/types'
|
|
5
|
+
import { withBackTarget } from '../router/backNavigation'
|
|
6
|
+
import { isGuidString } from '../utils/guid'
|
|
7
|
+
import type { ReportLookupStoreApi } from './lookupFilters'
|
|
8
|
+
import { appendSourceTrail, buildReportPageUrl, type ReportRouteContext, type ReportSourceTrail } from './navigation'
|
|
9
|
+
import type { ReportCellActionDto } from './types'
|
|
10
|
+
|
|
11
|
+
export type ReportLookupTargetArgs = {
|
|
12
|
+
hint: LookupSource | null
|
|
13
|
+
value: unknown
|
|
14
|
+
routeFullPath: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ReportCellActionNavigationOptions = {
|
|
18
|
+
currentReportContext?: ReportRouteContext | null
|
|
19
|
+
sourceTrail?: ReportSourceTrail | null
|
|
20
|
+
backTarget?: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ReportingFrameworkConfig = {
|
|
24
|
+
useLookupStore: () => ReportLookupStoreApi
|
|
25
|
+
resolveLookupTarget?: (args: ReportLookupTargetArgs) => Awaitable<string | null>
|
|
26
|
+
resolveCellActionUrl?: (
|
|
27
|
+
action: ReportCellActionDto | null | undefined,
|
|
28
|
+
options?: ReportCellActionNavigationOptions,
|
|
29
|
+
) => string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let configuredReporting: ReportingFrameworkConfig | null = null
|
|
33
|
+
|
|
34
|
+
function buildOpenReportActionUrl(
|
|
35
|
+
action: ReportCellActionDto,
|
|
36
|
+
options?: ReportCellActionNavigationOptions,
|
|
37
|
+
): string | null {
|
|
38
|
+
const reportCode = String(action.report?.reportCode ?? '').trim()
|
|
39
|
+
if (!reportCode) return null
|
|
40
|
+
|
|
41
|
+
return buildReportPageUrl(reportCode, {
|
|
42
|
+
context: {
|
|
43
|
+
reportCode,
|
|
44
|
+
request: {
|
|
45
|
+
parameters: action.report?.parameters ?? null,
|
|
46
|
+
filters: action.report?.filters ?? null,
|
|
47
|
+
layout: null,
|
|
48
|
+
offset: 0,
|
|
49
|
+
limit: 500,
|
|
50
|
+
cursor: null,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
sourceTrail: appendSourceTrail(options?.sourceTrail ?? null, options?.currentReportContext ?? null),
|
|
54
|
+
backTarget: options?.backTarget ?? null,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function withOptionalBackTarget(url: string, backTarget?: string | null): string {
|
|
59
|
+
return withBackTarget(url, backTarget ?? null)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function configureNgbReporting(config: ReportingFrameworkConfig) {
|
|
63
|
+
configuredReporting = config
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getConfiguredNgbReporting(): ReportingFrameworkConfig {
|
|
67
|
+
if (!configuredReporting) {
|
|
68
|
+
throw new Error('NGB reporting framework is not configured. Call configureNgbReporting(...) during app bootstrap.')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return configuredReporting
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function maybeGetConfiguredNgbReporting(): ReportingFrameworkConfig | null {
|
|
75
|
+
return configuredReporting
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function resolveReportLookupTarget(args: ReportLookupTargetArgs): Promise<string | null> {
|
|
79
|
+
const resolver = getConfiguredNgbReporting().resolveLookupTarget
|
|
80
|
+
if (!resolver) return null
|
|
81
|
+
return await resolver(args)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveReportCellActionUrl(
|
|
85
|
+
action: ReportCellActionDto | null | undefined,
|
|
86
|
+
options?: ReportCellActionNavigationOptions,
|
|
87
|
+
): string | null {
|
|
88
|
+
const configured = maybeGetConfiguredNgbReporting()
|
|
89
|
+
const configuredTarget = configured?.resolveCellActionUrl?.(action, options)
|
|
90
|
+
if (configuredTarget) return configuredTarget
|
|
91
|
+
if (!action || !action.kind) return null
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (action.kind === 'open_document') {
|
|
95
|
+
const documentType = String(action.documentType ?? '').trim()
|
|
96
|
+
const id = String(action.documentId ?? '').trim()
|
|
97
|
+
if (!documentType || !id || !isGuidString(id)) return null
|
|
98
|
+
return withOptionalBackTarget(buildDocumentFullPageUrl(documentType, id), options?.backTarget)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (action.kind === 'open_account') {
|
|
102
|
+
const accountId = String(action.accountId ?? '').trim()
|
|
103
|
+
if (!accountId || !isGuidString(accountId)) return null
|
|
104
|
+
return withOptionalBackTarget(buildChartOfAccountsPath({ panel: 'edit', id: accountId }), options?.backTarget)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (action.kind === 'open_catalog') {
|
|
108
|
+
const catalogType = String(action.catalogType ?? '').trim()
|
|
109
|
+
const id = String(action.catalogId ?? '').trim()
|
|
110
|
+
if (!catalogType || !id || !isGuidString(id)) return null
|
|
111
|
+
return withOptionalBackTarget(buildCatalogFullPageUrl(catalogType, id), options?.backTarget)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (action.kind === 'open_report') return buildOpenReportActionUrl(action, options)
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildLookupFieldTargetUrl } from '../lookup/navigation'
|
|
2
|
+
import { useLookupStore } from '../lookup/store'
|
|
3
|
+
import type { ReportingFrameworkConfig } from './config'
|
|
4
|
+
|
|
5
|
+
export function createDefaultNgbReportingConfig(): ReportingFrameworkConfig {
|
|
6
|
+
return {
|
|
7
|
+
useLookupStore: () => useLookupStore(),
|
|
8
|
+
resolveLookupTarget: async ({ hint, value, routeFullPath }) =>
|
|
9
|
+
await buildLookupFieldTargetUrl({
|
|
10
|
+
hint,
|
|
11
|
+
value,
|
|
12
|
+
route: { fullPath: routeFullPath },
|
|
13
|
+
}),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractLookupIds,
|
|
3
|
+
hydrateResolvedLookupItems,
|
|
4
|
+
searchResolvedLookupItems,
|
|
5
|
+
} from '../metadata/filtering'
|
|
6
|
+
import type { LookupStoreApi } from '../metadata/types'
|
|
7
|
+
import type {
|
|
8
|
+
ReportComposerDraft,
|
|
9
|
+
ReportComposerLookupItem,
|
|
10
|
+
ReportDefinitionDto,
|
|
11
|
+
ReportFilterValueDto,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
export type ReportLookupStoreApi = LookupStoreApi<ReportComposerLookupItem>
|
|
15
|
+
|
|
16
|
+
export async function searchReportLookupItems(
|
|
17
|
+
lookupStore: ReportLookupStoreApi,
|
|
18
|
+
lookup: NonNullable<NonNullable<ReportDefinitionDto['filters']>[number]['lookup']>,
|
|
19
|
+
query: string,
|
|
20
|
+
): Promise<ReportComposerLookupItem[]> {
|
|
21
|
+
return await searchResolvedLookupItems(lookupStore, lookup, query)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function hydrateReportLookupItemsFromFilters(
|
|
25
|
+
lookupStore: ReportLookupStoreApi,
|
|
26
|
+
definition: Pick<ReportDefinitionDto, 'filters'>,
|
|
27
|
+
draft: ReportComposerDraft,
|
|
28
|
+
filters: Record<string, ReportFilterValueDto> | null | undefined,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!filters) return
|
|
31
|
+
|
|
32
|
+
for (const field of definition.filters ?? []) {
|
|
33
|
+
const state = draft.filters[field.fieldCode]
|
|
34
|
+
const filterValue = filters[field.fieldCode]
|
|
35
|
+
if (!state || !field.lookup || !filterValue) continue
|
|
36
|
+
|
|
37
|
+
const ids = extractLookupIds(filterValue.value)
|
|
38
|
+
if (ids.length === 0) continue
|
|
39
|
+
|
|
40
|
+
state.items = await hydrateResolvedLookupItems(lookupStore, field.lookup, ids)
|
|
41
|
+
if (state.items.length > 0) state.raw = ''
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { withBackTarget } from '../router/backNavigation'
|
|
2
|
+
import { buildExecutionRequest } from './composer'
|
|
3
|
+
import type { ReportComposerDraft, ReportDefinitionDto, ReportExecutionRequestDto, ReportFilterValueDto } from './types'
|
|
4
|
+
|
|
5
|
+
export type ReportRouteContext = {
|
|
6
|
+
reportCode: string
|
|
7
|
+
reportName?: string | null
|
|
8
|
+
request: ReportExecutionRequestDto
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ReportSourceTrail = {
|
|
12
|
+
items: ReportRouteContext[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ReportTokenPayload = {
|
|
16
|
+
reportCode: string
|
|
17
|
+
parameters?: Record<string, string> | null
|
|
18
|
+
filters?: Record<string, ReportFilterValueDto> | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeQueryValue(value: unknown): string | null {
|
|
22
|
+
if (Array.isArray(value)) return normalizeQueryValue(value[0] ?? null)
|
|
23
|
+
const raw = String(value ?? '').trim()
|
|
24
|
+
return raw.length > 0 ? raw : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function encodeBase64UrlJson(value: unknown): string {
|
|
32
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value))
|
|
33
|
+
let binary = ''
|
|
34
|
+
bytes.forEach((byte) => {
|
|
35
|
+
binary += String.fromCharCode(byte)
|
|
36
|
+
})
|
|
37
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decodeBase64UrlJson<T>(value: unknown): T | null {
|
|
41
|
+
const raw = normalizeQueryValue(value)
|
|
42
|
+
if (!raw) return null
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const padded = raw.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((raw.length + 3) % 4)
|
|
46
|
+
const binary = atob(padded)
|
|
47
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
|
48
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as T
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeFilterValue(value: unknown): ReportFilterValueDto | null {
|
|
55
|
+
if (!isPlainObject(value) || !('value' in value)) return null
|
|
56
|
+
return {
|
|
57
|
+
value: value.value,
|
|
58
|
+
includeDescendants: !!value.includeDescendants,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeExecutionRequest(value: unknown): ReportExecutionRequestDto | null {
|
|
63
|
+
if (!isPlainObject(value)) return null
|
|
64
|
+
|
|
65
|
+
const parameters = isPlainObject(value.parameters)
|
|
66
|
+
? Object.fromEntries(Object.entries(value.parameters).map(([key, entry]) => [key, String(entry ?? '').trim()]).filter(([, entry]) => entry.length > 0))
|
|
67
|
+
: null
|
|
68
|
+
|
|
69
|
+
const filters = isPlainObject(value.filters)
|
|
70
|
+
? Object.fromEntries(Object.entries(value.filters).map(([key, entry]) => [key, normalizeFilterValue(entry)]).filter((entry): entry is [string, ReportFilterValueDto] => !!entry[1]))
|
|
71
|
+
: null
|
|
72
|
+
|
|
73
|
+
const layout = isPlainObject(value.layout) ? (value.layout as ReportExecutionRequestDto['layout']) : null
|
|
74
|
+
const variantCode = typeof value.variantCode === 'string' && value.variantCode.trim().length > 0 ? value.variantCode.trim() : null
|
|
75
|
+
const offset = typeof value.offset === 'number' && Number.isFinite(value.offset) ? value.offset : 0
|
|
76
|
+
const limit = typeof value.limit === 'number' && Number.isFinite(value.limit) ? value.limit : 500
|
|
77
|
+
const cursor = typeof value.cursor === 'string' && value.cursor.trim().length > 0 ? value.cursor.trim() : null
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
layout,
|
|
81
|
+
parameters: parameters && Object.keys(parameters).length > 0 ? parameters : null,
|
|
82
|
+
filters: filters && Object.keys(filters).length > 0 ? filters : null,
|
|
83
|
+
variantCode,
|
|
84
|
+
offset,
|
|
85
|
+
limit,
|
|
86
|
+
cursor,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeRouteContext(value: unknown): ReportRouteContext | null {
|
|
91
|
+
if (!isPlainObject(value)) return null
|
|
92
|
+
const reportCode = String(value.reportCode ?? '').trim()
|
|
93
|
+
if (!reportCode) return null
|
|
94
|
+
const request = normalizeExecutionRequest(value.request)
|
|
95
|
+
if (!request) return null
|
|
96
|
+
const reportName = typeof value.reportName === 'string' && value.reportName.trim().length > 0 ? value.reportName.trim() : null
|
|
97
|
+
return { reportCode, reportName, request }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function encodeReportRouteContextParam(value: ReportRouteContext | null | undefined): string | null {
|
|
101
|
+
return value ? encodeBase64UrlJson(value) : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function encodeReportSourceTrailParam(value: ReportSourceTrail | null | undefined): string | null {
|
|
105
|
+
return value && value.items.length > 0 ? encodeBase64UrlJson(value) : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildCurrentReportContext(definition: ReportDefinitionDto, draft: ReportComposerDraft): ReportRouteContext {
|
|
109
|
+
return {
|
|
110
|
+
reportCode: definition.reportCode,
|
|
111
|
+
reportName: definition.name,
|
|
112
|
+
request: buildExecutionRequest(definition, draft),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildReportPageUrl(
|
|
117
|
+
reportCode: string,
|
|
118
|
+
options?: {
|
|
119
|
+
context?: ReportRouteContext | null
|
|
120
|
+
sourceTrail?: ReportSourceTrail | null
|
|
121
|
+
backTarget?: string | null
|
|
122
|
+
},
|
|
123
|
+
): string {
|
|
124
|
+
const path = `/reports/${encodeURIComponent(reportCode)}`
|
|
125
|
+
const query = new URLSearchParams()
|
|
126
|
+
const contextParam = encodeReportRouteContextParam(options?.context)
|
|
127
|
+
const sourceTrailParam = encodeReportSourceTrailParam(options?.sourceTrail)
|
|
128
|
+
if (contextParam) query.set('ctx', contextParam)
|
|
129
|
+
if (sourceTrailParam) query.set('src', sourceTrailParam)
|
|
130
|
+
const suffix = query.toString()
|
|
131
|
+
const url = suffix ? `${path}?${suffix}` : path
|
|
132
|
+
return withBackTarget(url, options?.backTarget ?? null)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function decodeReportRouteContextParam(value: unknown): ReportRouteContext | null {
|
|
136
|
+
return normalizeRouteContext(decodeBase64UrlJson(value))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function decodeReportSourceTrailParam(value: unknown): ReportSourceTrail | null {
|
|
140
|
+
const decoded = decodeBase64UrlJson<{ items?: unknown }>(value)
|
|
141
|
+
if (!decoded || !Array.isArray(decoded.items)) return null
|
|
142
|
+
const items = decoded.items.map(normalizeRouteContext).filter((entry): entry is ReportRouteContext => !!entry)
|
|
143
|
+
return items.length > 0 ? { items } : null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function decodeReportDrilldownTarget(token: string | null | undefined): ReportRouteContext | null {
|
|
147
|
+
const raw = String(token ?? '').trim()
|
|
148
|
+
if (!raw.startsWith('report:')) return null
|
|
149
|
+
const payload = decodeBase64UrlJson<ReportTokenPayload>(raw.slice('report:'.length))
|
|
150
|
+
if (!payload || typeof payload.reportCode !== 'string' || payload.reportCode.trim().length === 0) return null
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
reportCode: payload.reportCode.trim(),
|
|
154
|
+
request: {
|
|
155
|
+
parameters: payload.parameters && Object.keys(payload.parameters).length > 0 ? payload.parameters : null,
|
|
156
|
+
filters: payload.filters && Object.keys(payload.filters).length > 0 ? payload.filters : null,
|
|
157
|
+
layout: null,
|
|
158
|
+
offset: 0,
|
|
159
|
+
limit: 500,
|
|
160
|
+
cursor: null,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function appendSourceTrail(sourceTrail: ReportSourceTrail | null | undefined, currentContext: ReportRouteContext | null | undefined): ReportSourceTrail | null {
|
|
166
|
+
const items = [...(sourceTrail?.items ?? [])]
|
|
167
|
+
if (currentContext) items.push(currentContext)
|
|
168
|
+
return items.length > 0 ? { items } : null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildBackToSourceUrl(sourceTrail: ReportSourceTrail | null | undefined, backTarget?: string | null): string | null {
|
|
172
|
+
const items = sourceTrail?.items ?? []
|
|
173
|
+
if (items.length === 0) return null
|
|
174
|
+
const target = items[items.length - 1]!
|
|
175
|
+
const priorItems = items.slice(0, -1)
|
|
176
|
+
return buildReportPageUrl(target.reportCode, {
|
|
177
|
+
context: target,
|
|
178
|
+
sourceTrail: priorItems.length > 0 ? { items: priorItems } : null,
|
|
179
|
+
backTarget: backTarget ?? null,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { optionLabelForFilter } from '../metadata/filtering'
|
|
2
|
+
import { normalizeDateOnlyValue } from '../utils/dateValues'
|
|
3
|
+
import { slugifyVariantCode } from './composer'
|
|
4
|
+
import type {
|
|
5
|
+
ReportComposerDraft,
|
|
6
|
+
ReportDefinitionDto,
|
|
7
|
+
ReportFilterFieldDto,
|
|
8
|
+
ReportVariantDto,
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
export type ReportPageBadge = {
|
|
12
|
+
key: string
|
|
13
|
+
text: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasFilterStateValue(field: ReportFilterFieldDto, composer: ReportComposerDraft): boolean {
|
|
17
|
+
const state = composer.filters[field.fieldCode]
|
|
18
|
+
if (!state) return false
|
|
19
|
+
if (state.items.length > 0) return true
|
|
20
|
+
return state.raw.trim().length > 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function canAutoRunReport(definition: ReportDefinitionDto, composer: ReportComposerDraft): boolean {
|
|
24
|
+
const missingParameter = (definition.parameters ?? []).some((parameter) => parameter.isRequired && String(composer.parameters[parameter.code] ?? '').trim().length === 0)
|
|
25
|
+
if (missingParameter) return false
|
|
26
|
+
return !(definition.filters ?? []).some((field) => field.isRequired && !hasFilterStateValue(field, composer))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeParameterDataType(dataType: string): string {
|
|
30
|
+
return dataType.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isInlineDateParameterDataType(dataType: string): boolean {
|
|
34
|
+
const normalized = normalizeParameterDataType(dataType)
|
|
35
|
+
return normalized === 'date' || normalized === 'date_only' || normalized === 'date_time_utc'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parameterLabel(parameter: { code: string; label?: string | null }): string {
|
|
39
|
+
return String(parameter.label ?? parameter.code).trim() || String(parameter.code ?? '').trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeReportDateValue(value: unknown): string | null {
|
|
43
|
+
return normalizeDateOnlyValue(String(value ?? '').trim())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCode(value: string): string {
|
|
47
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function tryResolveOptionLabel(field: ReportFilterFieldDto, rawValue: string): string {
|
|
51
|
+
const normalized = rawValue.trim()
|
|
52
|
+
if (normalized.length === 0) return normalized
|
|
53
|
+
return optionLabelForFilter(field, normalized) || normalized
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function chooseAvailableVariantCode(
|
|
57
|
+
name: string,
|
|
58
|
+
variants: readonly ReportVariantDto[],
|
|
59
|
+
exceptVariantCode?: string | null,
|
|
60
|
+
): string {
|
|
61
|
+
const base = slugifyVariantCode(name)
|
|
62
|
+
|
|
63
|
+
let candidate = base
|
|
64
|
+
let suffix = 2
|
|
65
|
+
const except = String(exceptVariantCode ?? '').trim()
|
|
66
|
+
|
|
67
|
+
while (variants.some((variant) => variant.variantCode === candidate && variant.variantCode !== except)) {
|
|
68
|
+
candidate = `${base}-${suffix}`
|
|
69
|
+
suffix += 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return candidate
|
|
73
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readStorageJsonOrNull,
|
|
3
|
+
readStorageString,
|
|
4
|
+
removeStorageItem,
|
|
5
|
+
writeStorageJson,
|
|
6
|
+
writeStorageString,
|
|
7
|
+
} from '../utils/storage'
|
|
8
|
+
|
|
9
|
+
import type { ReportExecutionResponseDto } from './types'
|
|
10
|
+
|
|
11
|
+
const EXECUTION_PREFIX = 'ngb.report.page.execution:'
|
|
12
|
+
const SCROLL_PREFIX = 'ngb.report.page.scroll:'
|
|
13
|
+
|
|
14
|
+
export type ReportPageExecutionSnapshot = {
|
|
15
|
+
response: ReportExecutionResponseDto
|
|
16
|
+
consumedCursors: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeKey(key: string | null | undefined): string | null {
|
|
20
|
+
const normalized = String(key ?? '').trim()
|
|
21
|
+
return normalized.length > 0 ? normalized : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function executionStorageKey(routeStateKey: string | null | undefined): string | null {
|
|
25
|
+
const normalized = normalizeKey(routeStateKey)
|
|
26
|
+
return normalized ? `${EXECUTION_PREFIX}${normalized}` : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scrollStorageKey(routeStateKey: string | null | undefined): string | null {
|
|
30
|
+
const normalized = normalizeKey(routeStateKey)
|
|
31
|
+
return normalized ? `${SCROLL_PREFIX}${normalized}` : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function saveReportPageExecutionSnapshot(
|
|
35
|
+
routeStateKey: string | null | undefined,
|
|
36
|
+
response: ReportExecutionResponseDto,
|
|
37
|
+
consumedCursors: string[],
|
|
38
|
+
) {
|
|
39
|
+
const storageKey = executionStorageKey(routeStateKey ?? null)
|
|
40
|
+
if (!storageKey) return
|
|
41
|
+
|
|
42
|
+
void writeStorageJson('session', storageKey, {
|
|
43
|
+
response,
|
|
44
|
+
consumedCursors: Array.from(new Set((consumedCursors ?? []).map((entry) => String(entry ?? '').trim()).filter((entry) => entry.length > 0))),
|
|
45
|
+
} satisfies ReportPageExecutionSnapshot)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadReportPageExecutionSnapshot(routeStateKey: string | null | undefined): ReportPageExecutionSnapshot | null {
|
|
49
|
+
const storageKey = executionStorageKey(routeStateKey ?? null)
|
|
50
|
+
if (!storageKey) return null
|
|
51
|
+
|
|
52
|
+
const snapshot = readStorageJsonOrNull<ReportPageExecutionSnapshot>('session', storageKey)
|
|
53
|
+
if (!snapshot?.response?.sheet) return null
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
response: snapshot.response,
|
|
57
|
+
consumedCursors: Array.from(new Set((snapshot.consumedCursors ?? []).map((entry) => String(entry ?? '').trim()).filter((entry) => entry.length > 0))),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function clearReportPageExecutionSnapshot(routeStateKey: string | null | undefined) {
|
|
62
|
+
const storageKey = executionStorageKey(routeStateKey ?? null)
|
|
63
|
+
if (!storageKey) return
|
|
64
|
+
removeStorageItem('session', storageKey)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function saveReportPageScrollTop(routeStateKey: string | null | undefined, scrollTop: number) {
|
|
68
|
+
const storageKey = scrollStorageKey(routeStateKey ?? null)
|
|
69
|
+
if (!storageKey) return
|
|
70
|
+
|
|
71
|
+
const normalized = Number.isFinite(scrollTop) && scrollTop > 0 ? Math.floor(scrollTop) : 0
|
|
72
|
+
if (normalized <= 0) {
|
|
73
|
+
removeStorageItem('session', storageKey)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void writeStorageString('session', storageKey, String(normalized))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadReportPageScrollTop(routeStateKey: string | null | undefined): number {
|
|
81
|
+
const storageKey = scrollStorageKey(routeStateKey ?? null)
|
|
82
|
+
if (!storageKey) return 0
|
|
83
|
+
|
|
84
|
+
const parsed = Number(readStorageString('session', storageKey) ?? '')
|
|
85
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearReportPageScrollTop(routeStateKey: string | null | undefined) {
|
|
89
|
+
const storageKey = scrollStorageKey(routeStateKey ?? null)
|
|
90
|
+
if (!storageKey) return
|
|
91
|
+
removeStorageItem('session', storageKey)
|
|
92
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ReportRowKind, type ReportExecutionRequestDto, type ReportExecutionResponseDto, type ReportSheetDto, type ReportSheetRowDto } from './types'
|
|
2
|
+
import { stableEquals } from '../utils/stableValue'
|
|
3
|
+
|
|
4
|
+
function normalizeCursor(value: string | null | undefined): string | null {
|
|
5
|
+
const normalized = String(value ?? '').trim()
|
|
6
|
+
return normalized.length > 0 ? normalized : null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isGrandTotalRow(row: ReportSheetRowDto | null | undefined): boolean {
|
|
10
|
+
if (!row) return false
|
|
11
|
+
if (row.rowKind === ReportRowKind.Total) return true
|
|
12
|
+
return String(row.semanticRole ?? '').trim().toLowerCase() === 'grand_total'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function countLoadedReportRows(sheet: ReportSheetDto | null | undefined): number {
|
|
16
|
+
const rows = sheet?.rows ?? []
|
|
17
|
+
return rows.reduce((count, row) => count + (isGrandTotalRow(row) ? 0 : 1), 0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cloneSheetWithRows(sheet: ReportSheetDto, rows: ReportSheetDto['rows']): ReportSheetDto {
|
|
21
|
+
return {
|
|
22
|
+
columns: [...(sheet.columns ?? [])],
|
|
23
|
+
rows,
|
|
24
|
+
meta: sheet.meta ?? null,
|
|
25
|
+
headerRows: sheet.headerRows ? [...sheet.headerRows] : null,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function areSheetsAppendCompatible(left: ReportSheetDto, right: ReportSheetDto): boolean {
|
|
30
|
+
return stableEquals(left.columns ?? [], right.columns ?? [])
|
|
31
|
+
&& stableEquals(left.headerRows ?? [], right.headerRows ?? [])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function canAppendReportResponse(response: ReportExecutionResponseDto | null | undefined): boolean {
|
|
35
|
+
return !!response?.hasMore && !!normalizeCursor(response.nextCursor)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildAppendRequest(baseRequest: ReportExecutionRequestDto, nextCursor: string): ReportExecutionRequestDto {
|
|
39
|
+
return {
|
|
40
|
+
...baseRequest,
|
|
41
|
+
offset: 0,
|
|
42
|
+
limit: Math.max(1, baseRequest.limit ?? 500),
|
|
43
|
+
cursor: normalizeCursor(nextCursor),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function mergePagedReportResponses(
|
|
48
|
+
current: ReportExecutionResponseDto,
|
|
49
|
+
next: ReportExecutionResponseDto,
|
|
50
|
+
): ReportExecutionResponseDto {
|
|
51
|
+
if (!areSheetsAppendCompatible(current.sheet, next.sheet)) {
|
|
52
|
+
throw new Error('Paged report append returned an incompatible sheet shape.')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mergedRows = [...(current.sheet.rows ?? []), ...(next.sheet.rows ?? [])]
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
sheet: cloneSheetWithRows(current.sheet, mergedRows),
|
|
59
|
+
offset: current.offset,
|
|
60
|
+
limit: current.limit,
|
|
61
|
+
total: next.total ?? current.total ?? mergedRows.length,
|
|
62
|
+
hasMore: next.hasMore,
|
|
63
|
+
nextCursor: normalizeCursor(next.nextCursor),
|
|
64
|
+
diagnostics: next.diagnostics ?? current.diagnostics,
|
|
65
|
+
}
|
|
66
|
+
}
|