@opencorestack/opengridx 2.1.0 → 3.0.1
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/CHANGELOG.md +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -166
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +6 -7
- package/docs/components/datagrid.md +15 -13
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +90 -38
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +126 -39
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +19 -4
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +279 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +104 -5
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +17 -5
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +82 -51
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +55 -5
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
import type { GridColDef, GridRowModel, GridAggregationModel, GridGroupedExportRow } from '../../types';
|
|
3
|
-
import {
|
|
2
|
+
import type { GridColDef, GridRowId, GridRowModel, GridValidRowModel, GridAggregationModel, GridGroupedExportRow } from '../../types';
|
|
3
|
+
import { groupHeaderLabel } from './groupLabel';
|
|
4
|
+
import {
|
|
5
|
+
aggregationForExport,
|
|
6
|
+
formatExportAggregate,
|
|
7
|
+
formatExportValue,
|
|
8
|
+
getExportColumns,
|
|
9
|
+
getRawExportValue,
|
|
10
|
+
hasSelection,
|
|
11
|
+
isCountAggregation,
|
|
12
|
+
normalizeAggregateValue,
|
|
13
|
+
pickSelectedRows,
|
|
14
|
+
sanitizeSheetName,
|
|
15
|
+
} from './exportShared';
|
|
4
16
|
|
|
5
17
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
6
18
|
|
|
@@ -12,10 +24,10 @@ export interface ExcelColumnStyle {
|
|
|
12
24
|
/** Horizontal alignment override */
|
|
13
25
|
alignment?: 'left' | 'center' | 'right';
|
|
14
26
|
/**
|
|
15
|
-
* If true, the cell value (
|
|
16
|
-
*
|
|
17
|
-
* Requires the image server to serve CORS headers. Falls back to the
|
|
18
|
-
*
|
|
27
|
+
* If true, the cell value (an image URL string, after `valueGetter`) is fetched and embedded
|
|
28
|
+
* as an inline image in the cell. PNG, JPEG and GIF are supported (detected from the bytes).
|
|
29
|
+
* Requires the image server to serve CORS headers. Falls back to the raw URL string if the
|
|
30
|
+
* fetch fails or the image is in another format (a warning is logged to the console).
|
|
19
31
|
*/
|
|
20
32
|
embedImage?: boolean;
|
|
21
33
|
/** Width of the embedded image in pixels (default: 40) */
|
|
@@ -25,9 +37,12 @@ export interface ExcelColumnStyle {
|
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export interface ExcelSheetDefinition {
|
|
28
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Sheet tab name. Characters Excel rejects (`\ / ? * : [ ]`) become `-`, the name is cut to
|
|
42
|
+
* 31 characters, and a duplicate name gets a " (2)" suffix.
|
|
43
|
+
*/
|
|
29
44
|
name: string;
|
|
30
|
-
/** Which rows to include. Default: 'all' */
|
|
45
|
+
/** Which rows to include. `'selected'` writes only `selectedRows` (none when nothing is selected). Default: 'all' */
|
|
31
46
|
rows?: 'all' | 'selected';
|
|
32
47
|
/** Include header row. Default: true */
|
|
33
48
|
includeHeaders?: boolean;
|
|
@@ -41,7 +56,7 @@ export interface ExcelSheetDefinition {
|
|
|
41
56
|
alternateRowColor?: string | false;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
export interface ExcelAdvancedExportOptions {
|
|
59
|
+
export interface ExcelAdvancedExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
45
60
|
/** Output filename (default: 'export.xlsx') */
|
|
46
61
|
fileName?: string;
|
|
47
62
|
/**
|
|
@@ -51,7 +66,7 @@ export interface ExcelAdvancedExportOptions {
|
|
|
51
66
|
* Built-in special sheet type: `{ type: 'summary' }` renders a
|
|
52
67
|
* standalone aggregation sheet.
|
|
53
68
|
*/
|
|
54
|
-
sheets?: (ExcelSheetDefinition | { type: 'summary'; name?: string })[];
|
|
69
|
+
sheets?: (ExcelSheetDefinition | { type: 'summary'; name?: string })[];
|
|
55
70
|
/** Per-column style overrides, keyed by field name */
|
|
56
71
|
columnStyles?: Record<string, ExcelColumnStyle>;
|
|
57
72
|
/** Header row fill color (default: '#f1f5f9') */
|
|
@@ -66,8 +81,13 @@ export interface ExcelAdvancedExportOptions {
|
|
|
66
81
|
aggregationResult?: Record<string, unknown> | null;
|
|
67
82
|
/** Current aggregation model */
|
|
68
83
|
aggregationModel?: GridAggregationModel | null;
|
|
69
|
-
/** Currently selected row IDs */
|
|
84
|
+
/** Currently selected row IDs, written by `rows: 'selected'` sheets */
|
|
70
85
|
selectedRows?: (string | number)[];
|
|
86
|
+
/**
|
|
87
|
+
* The grid's `getRowId`, when it has one: `selectedRows` holds those ids, and the grid does not
|
|
88
|
+
* copy them onto `row.id` (v3.0+). Defaults to `row.id`.
|
|
89
|
+
*/
|
|
90
|
+
getRowId?: (row: R) => GridRowId;
|
|
71
91
|
/**
|
|
72
92
|
* Grouped row structure, typically from `apiRef.current.getGroupedExportRows()`.
|
|
73
93
|
* When provided, sheets with `rows: 'all'` are written in grouped order: group-header,
|
|
@@ -83,9 +103,21 @@ export interface ExcelAdvancedExportOptions {
|
|
|
83
103
|
groupSubtotalFillColor?: string;
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
// ─── Cell
|
|
106
|
+
// ─── Cell values ──────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** The only values handed to ExcelJS. Objects are never passed through, because ExcelJS reads
|
|
109
|
+
* `{ formula }`, `{ hyperlink }`, `{ richText }` and `{ error }` objects as live cell content. */
|
|
110
|
+
type ExcelCellValue = string | number | boolean | Date | null;
|
|
111
|
+
|
|
112
|
+
/** A subtotal / total cell value, with the number format it needs when it differs from the column's. */
|
|
113
|
+
interface AggregateCell {
|
|
114
|
+
value: ExcelCellValue;
|
|
115
|
+
numFmt?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type CellKind = 'number' | 'boolean' | 'date' | 'string';
|
|
87
119
|
|
|
88
|
-
function
|
|
120
|
+
function cellKind(colDef: GridColDef<GridRowModel>, value: unknown): CellKind {
|
|
89
121
|
if (colDef.type === 'number') return 'number';
|
|
90
122
|
if (colDef.type === 'boolean') return 'boolean';
|
|
91
123
|
if (colDef.type === 'date') return 'date';
|
|
@@ -95,14 +127,131 @@ function inferCellType(colDef: GridColDef<GridRowModel>, value: unknown): 'numbe
|
|
|
95
127
|
return 'string';
|
|
96
128
|
}
|
|
97
129
|
|
|
98
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Excel date serials carry no timezone and are read as wall-clock time, while ExcelJS converts a
|
|
132
|
+
* Date from its UTC instant. Re-anchor the local wall-clock fields at UTC so the cell shows the
|
|
133
|
+
* date and time the grid shows. Invalid dates become null.
|
|
134
|
+
*/
|
|
135
|
+
function toExcelDate(date: Date): Date | null {
|
|
136
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
137
|
+
return new Date(Date.UTC(
|
|
138
|
+
date.getFullYear(), date.getMonth(), date.getDate(),
|
|
139
|
+
date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds(),
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ISO_DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
144
|
+
const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/i;
|
|
145
|
+
const NUMERIC_STRING = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i;
|
|
146
|
+
|
|
147
|
+
/** A date-column value as a Date: null when missing or invalid, undefined when not a date at all. */
|
|
148
|
+
function dateCellValue(value: unknown): Date | null | undefined {
|
|
149
|
+
if (value instanceof Date) return toExcelDate(value);
|
|
150
|
+
if (typeof value === 'number') return Number.isFinite(value) ? toExcelDate(new Date(value)) : null;
|
|
151
|
+
if (typeof value !== 'string') return undefined;
|
|
152
|
+
const text = value.trim();
|
|
153
|
+
const dateOnly = ISO_DATE_ONLY.exec(text);
|
|
154
|
+
if (dateOnly) {
|
|
155
|
+
// A calendar date: already wall-clock, so build it at UTC directly.
|
|
156
|
+
const [year, month, day] = [Number(dateOnly[1]), Number(dateOnly[2]), Number(dateOnly[3])];
|
|
157
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
158
|
+
return date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? date : undefined;
|
|
159
|
+
}
|
|
160
|
+
if (ISO_DATE_TIME.test(text)) {
|
|
161
|
+
const date = new Date(text.replace(' ', 'T'));
|
|
162
|
+
return Number.isNaN(date.getTime()) ? undefined : toExcelDate(date);
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A native value for a number / boolean / date cell: null for an empty cell (missing, NaN,
|
|
169
|
+
* Infinity, Invalid Date), undefined when the value is not of that kind (e.g. 'n/a' in a
|
|
170
|
+
* number column).
|
|
171
|
+
*/
|
|
172
|
+
function typedCellValue(kind: Exclude<CellKind, 'string'>, value: unknown): number | boolean | Date | null | undefined {
|
|
173
|
+
if (value == null || value === '') return null;
|
|
174
|
+
if (kind === 'number') {
|
|
175
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
176
|
+
if (typeof value === 'bigint') {
|
|
177
|
+
const n = Number(value);
|
|
178
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'string' && NUMERIC_STRING.test(value.trim())) {
|
|
181
|
+
const n = Number(value.trim());
|
|
182
|
+
return Number.isFinite(n) ? n : undefined;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'boolean') {
|
|
187
|
+
if (typeof value === 'boolean') return value;
|
|
188
|
+
if (typeof value === 'string') {
|
|
189
|
+
const text = value.trim().toLowerCase();
|
|
190
|
+
if (text === 'true') return true;
|
|
191
|
+
if (text === 'false') return false;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return dateCellValue(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Any value as inert cell content: primitives and Dates as themselves, objects as JSON text. */
|
|
199
|
+
function inertCellValue(value: unknown): ExcelCellValue {
|
|
200
|
+
if (value == null) return null;
|
|
201
|
+
if (typeof value === 'string' || typeof value === 'boolean') return value;
|
|
202
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
203
|
+
if (value instanceof Date) return toExcelDate(value);
|
|
204
|
+
if (typeof value === 'object') {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.stringify(value) ?? String(value);
|
|
207
|
+
} catch {
|
|
208
|
+
return String(value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return String(value);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The value of a data cell. Number, boolean and date cells are native (their `valueFormatter` is
|
|
216
|
+
* not used, so Excel's number format applies); a value that is missing or not of the column's
|
|
217
|
+
* kind falls back to the text the grid shows. Other cells get the `valueFormatter` text.
|
|
218
|
+
*/
|
|
219
|
+
function dataCellValue(row: GridRowModel, col: GridColDef<GridRowModel>): ExcelCellValue {
|
|
220
|
+
const raw = getRawExportValue(row, col);
|
|
221
|
+
const kind = cellKind(col, raw);
|
|
222
|
+
if (kind !== 'string') {
|
|
223
|
+
const typed = typedCellValue(kind, raw);
|
|
224
|
+
if (typed != null) return typed;
|
|
225
|
+
if (!col.valueFormatter) return typed === null ? null : inertCellValue(raw);
|
|
226
|
+
}
|
|
227
|
+
return col.valueFormatter ? formatExportValue(row, col, raw) : inertCellValue(raw);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Number formats ──────────────────────────────────────────────────────────
|
|
99
231
|
|
|
100
|
-
|
|
101
|
-
|
|
232
|
+
const INTEGER_FORMAT = '#,##0';
|
|
233
|
+
const DECIMAL_FORMAT = '#,##0.##';
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Default format for a number-column value. '#,##0.##' alone shows whole numbers with a trailing
|
|
237
|
+
* decimal point in Excel ("5."), so whole numbers get '#,##0'.
|
|
238
|
+
*/
|
|
239
|
+
function defaultNumberFormat(value: number): string {
|
|
240
|
+
return Number.isInteger(value) ? INTEGER_FORMAT : DECIMAL_FORMAT;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function columnNumFmt(colDef: GridColDef<GridRowModel>, style: ExcelColumnStyle | undefined): string | undefined {
|
|
244
|
+
if (style?.numFmt) return style.numFmt;
|
|
102
245
|
if (colDef.type === 'date') return 'yyyy-mm-dd';
|
|
103
246
|
return undefined;
|
|
104
247
|
}
|
|
105
248
|
|
|
249
|
+
/** The number format for a native cell value, or undefined to keep the column's format. */
|
|
250
|
+
function cellNumFmt(colDef: GridColDef<GridRowModel>, style: ExcelColumnStyle | undefined, value: ExcelCellValue): string | undefined {
|
|
251
|
+
if (style?.numFmt || typeof value !== 'number' || colDef.type !== 'number') return undefined;
|
|
252
|
+
return defaultNumberFormat(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
106
255
|
// ─── Column display width (chars) ────────────────────────────────────────────
|
|
107
256
|
|
|
108
257
|
function colWidth(colDef: GridColDef<GridRowModel>, override?: number): number {
|
|
@@ -115,48 +264,49 @@ function colWidth(colDef: GridColDef<GridRowModel>, override?: number): number {
|
|
|
115
264
|
|
|
116
265
|
type SupportedImageExt = 'png' | 'jpeg' | 'gif';
|
|
117
266
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
if (
|
|
125
|
-
return '
|
|
267
|
+
/**
|
|
268
|
+
* The image format from the file's magic bytes. Excel can only show PNG, JPEG and GIF here; SVG,
|
|
269
|
+
* WebP, AVIF and anything else return null so the URL text is written instead.
|
|
270
|
+
*/
|
|
271
|
+
function detectImageExtension(bytes: Uint8Array): SupportedImageExt | null {
|
|
272
|
+
if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) return 'png';
|
|
273
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'jpeg';
|
|
274
|
+
if (bytes.length >= 6 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) return 'gif';
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The image URL of an embedImage cell: the cell value after `valueGetter`. */
|
|
279
|
+
function imageUrlOf(row: GridRowModel, col: GridColDef<GridRowModel>): string | null {
|
|
280
|
+
const value = getRawExportValue(row, col);
|
|
281
|
+
return typeof value === 'string' && value ? value : null;
|
|
126
282
|
}
|
|
127
283
|
|
|
128
284
|
/**
|
|
129
285
|
* Fetch a list of image URLs in parallel.
|
|
130
|
-
* CORS failures are logged and skipped — raw URL text is written to the
|
|
131
|
-
* instead so data is never silently lost.
|
|
286
|
+
* CORS failures and unsupported formats are logged and skipped — raw URL text is written to the
|
|
287
|
+
* cell instead so data is never silently lost.
|
|
132
288
|
*/
|
|
133
289
|
async function fetchImages(
|
|
134
290
|
urls: string[]
|
|
135
291
|
): Promise<Map<string, { buffer: ArrayBuffer; extension: SupportedImageExt }>> {
|
|
136
292
|
const settled = await Promise.allSettled(
|
|
137
293
|
urls.map(async (url) => {
|
|
138
|
-
|
|
294
|
+
let bytes: Uint8Array;
|
|
139
295
|
if (url.startsWith('data:')) {
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
else if (mime.includes('gif')) ext = 'gif';
|
|
151
|
-
return { url, buffer: uint8.buffer as ArrayBuffer, ext };
|
|
296
|
+
// ── data: URI — decode base64 directly, no network needed ──────────
|
|
297
|
+
const b64 = url.slice(url.indexOf(',') + 1);
|
|
298
|
+
const binary = atob(b64);
|
|
299
|
+
bytes = new Uint8Array(binary.length);
|
|
300
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
301
|
+
} else {
|
|
302
|
+
// ── Remote URL — fetch with CORS ──────────────────────────────────
|
|
303
|
+
const res = await fetch(url, { mode: 'cors' });
|
|
304
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
305
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
152
306
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
157
|
-
const buffer = await res.arrayBuffer();
|
|
158
|
-
const ext = imageExtension(url, res.headers.get('content-type'));
|
|
159
|
-
return { url, buffer, ext };
|
|
307
|
+
const ext = detectImageExtension(bytes);
|
|
308
|
+
if (!ext) throw new Error('unsupported image format (only PNG, JPEG and GIF can be embedded)');
|
|
309
|
+
return { url, buffer: bytes.slice().buffer, ext };
|
|
160
310
|
})
|
|
161
311
|
);
|
|
162
312
|
const map = new Map<string, { buffer: ArrayBuffer; extension: SupportedImageExt }>();
|
|
@@ -166,7 +316,7 @@ async function fetchImages(
|
|
|
166
316
|
} else {
|
|
167
317
|
console.warn(
|
|
168
318
|
`[exportToExcelAdvanced] Cannot embed image "${urls[i]}" ` +
|
|
169
|
-
`(CORS/network error — raw URL written as fallback):`,
|
|
319
|
+
`(CORS/network error or unsupported format — raw URL written as fallback):`,
|
|
170
320
|
result.reason
|
|
171
321
|
);
|
|
172
322
|
}
|
|
@@ -184,6 +334,8 @@ async function fetchImages(
|
|
|
184
334
|
* ExcelJS is loaded lazily so it does not affect bundle size for apps
|
|
185
335
|
* that only use CSV/JSON export.
|
|
186
336
|
*/
|
|
337
|
+
export function exportToExcelAdvanced<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: ExcelAdvancedExportOptions<R>): Promise<void>;
|
|
338
|
+
export function exportToExcelAdvanced<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: ExcelAdvancedExportOptions<R>): Promise<void>;
|
|
187
339
|
export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
188
340
|
rows: R[],
|
|
189
341
|
columns: GridColDef<R>[],
|
|
@@ -200,6 +352,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
200
352
|
aggregationResult = null,
|
|
201
353
|
aggregationModel = null,
|
|
202
354
|
selectedRows,
|
|
355
|
+
getRowId,
|
|
203
356
|
groupedRows,
|
|
204
357
|
groupHeaderFillColor = '#e8eaf6',
|
|
205
358
|
groupSubtotalFillColor = '#f0f4ff',
|
|
@@ -211,26 +364,40 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
211
364
|
workbook.creator = 'OpenGridX';
|
|
212
365
|
workbook.created = new Date();
|
|
213
366
|
|
|
214
|
-
|
|
215
|
-
const exportColumns: GridColDef<GridRowModel>[] = (
|
|
216
|
-
col.exportable !== false &&
|
|
217
|
-
col.field !== '__check__' &&
|
|
218
|
-
col.field !== '__actions__' &&
|
|
219
|
-
!col.isSpacer
|
|
220
|
-
);
|
|
367
|
+
const allColumns = columns as unknown as GridColDef<GridRowModel>[];
|
|
368
|
+
const exportColumns: GridColDef<GridRowModel>[] = getExportColumns(allColumns);
|
|
221
369
|
|
|
222
370
|
const allRows: GridRowModel[] = rows as GridRowModel[];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: rows) as GridRowModel[];
|
|
371
|
+
// A 'selected' sheet writes the selection only: an empty selection is an empty sheet.
|
|
372
|
+
const sRows: GridRowModel[] = hasSelection(selectedRows) ? pickSelectedRows(allRows, selectedRows, getRowId) : [];
|
|
226
373
|
|
|
227
374
|
// Normalise sheet definitions
|
|
228
375
|
const resolvedSheets: (ExcelSheetDefinition | { type: 'summary'; name?: string })[] =
|
|
229
376
|
sheetDefs ?? [{ name: 'Data', rows: 'all', includeHeaders: true, includeSummary: false }];
|
|
377
|
+
const usedSheetNames = new Set<string>();
|
|
230
378
|
|
|
231
379
|
// Workbook-level image cache — one fetch per URL, reused across multiple sheets
|
|
232
380
|
const imageCache = new Map<string, number>();
|
|
233
381
|
|
|
382
|
+
/**
|
|
383
|
+
* A subtotal / total cell. Numbers stay numeric: `count` / `unique` in a plain integer format
|
|
384
|
+
* (not the column's date or currency format), `min` / `max` of a date column as a date, and
|
|
385
|
+
* other numbers in the column's format. Anything else is the formatted text.
|
|
386
|
+
*/
|
|
387
|
+
const aggregateCell = (col: GridColDef<GridRowModel>, values: Record<string, unknown>): AggregateCell => {
|
|
388
|
+
const raw = values[col.field];
|
|
389
|
+
const fnName = aggregationModel?.[col.field];
|
|
390
|
+
if (raw == null) return { value: null };
|
|
391
|
+
const normalized = normalizeAggregateValue(col, fnName, raw);
|
|
392
|
+
if (typeof normalized === 'number') {
|
|
393
|
+
if (!Number.isFinite(normalized)) return { value: null };
|
|
394
|
+
if (isCountAggregation(fnName)) return { value: normalized, numFmt: INTEGER_FORMAT };
|
|
395
|
+
return { value: normalized, numFmt: cellNumFmt(col, columnStyles[col.field], normalized) };
|
|
396
|
+
}
|
|
397
|
+
if (normalized instanceof Date) return { value: toExcelDate(normalized) };
|
|
398
|
+
return { value: formatExportAggregate(col, fnName, raw, values) };
|
|
399
|
+
};
|
|
400
|
+
|
|
234
401
|
// ─── Build each sheet ────────────────────────────────────────────────────
|
|
235
402
|
|
|
236
403
|
for (const sheetDef of resolvedSheets) {
|
|
@@ -238,7 +405,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
238
405
|
// ── Summary-only sheet ───────────────────────────────────────────────
|
|
239
406
|
if ('type' in sheetDef && sheetDef.type === 'summary') {
|
|
240
407
|
if (!aggregationResult || !aggregationModel) continue;
|
|
241
|
-
const ws = workbook.addWorksheet(sheetDef.name ?? 'Summary');
|
|
408
|
+
const ws = workbook.addWorksheet(sanitizeSheetName(sheetDef.name ?? 'Summary', usedSheetNames));
|
|
242
409
|
|
|
243
410
|
// Title row
|
|
244
411
|
ws.addRow(['Summary / Aggregation Totals']);
|
|
@@ -254,18 +421,15 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
254
421
|
});
|
|
255
422
|
|
|
256
423
|
exportColumns.forEach(col => {
|
|
257
|
-
const fn = aggregationModel
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
const row = ws.addRow([col.headerName ?? col.field, fn.toUpperCase(), formatted]);
|
|
267
|
-
row.getCell(3).alignment = { horizontal: 'right' };
|
|
268
|
-
row.getCell(3).font = { bold: true, size: bodyFontSize };
|
|
424
|
+
const fn = aggregationModel[col.field];
|
|
425
|
+
if (fn == null || aggregationResult[col.field] == null) return;
|
|
426
|
+
const { value, numFmt } = aggregateCell(col, aggregationResult);
|
|
427
|
+
const row = ws.addRow([col.headerName ?? col.field, fn.toUpperCase(), value]);
|
|
428
|
+
const valueCell = row.getCell(3);
|
|
429
|
+
const format = numFmt ?? columnNumFmt(col, columnStyles[col.field]);
|
|
430
|
+
if (format) valueCell.numFmt = format;
|
|
431
|
+
valueCell.alignment = { horizontal: 'right' };
|
|
432
|
+
valueCell.font = { bold: true, size: bodyFontSize };
|
|
269
433
|
});
|
|
270
434
|
|
|
271
435
|
ws.getColumn(1).width = 20;
|
|
@@ -300,10 +464,10 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
300
464
|
const seen = new Set<string>();
|
|
301
465
|
rowsToExport.forEach(row => {
|
|
302
466
|
imageColumns.forEach(col => {
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
urlsNeeded.push(
|
|
306
|
-
seen.add(
|
|
467
|
+
const url = imageUrlOf(row, col);
|
|
468
|
+
if (url && !imageCache.has(url) && !seen.has(url)) {
|
|
469
|
+
urlsNeeded.push(url);
|
|
470
|
+
seen.add(url);
|
|
307
471
|
}
|
|
308
472
|
});
|
|
309
473
|
});
|
|
@@ -316,30 +480,34 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
316
480
|
}
|
|
317
481
|
}
|
|
318
482
|
|
|
319
|
-
const ws = workbook.addWorksheet(name);
|
|
483
|
+
const ws = workbook.addWorksheet(sanitizeSheetName(name, usedSheetNames));
|
|
320
484
|
|
|
321
485
|
// ── Set column definitions ───────────────────────────────────────────
|
|
322
486
|
ws.columns = exportColumns.map(col => ({
|
|
323
487
|
key: col.field,
|
|
324
488
|
width: colWidth(col, columnStyles[col.field]?.width),
|
|
325
489
|
style: {
|
|
326
|
-
numFmt: columnStyles[col.field]
|
|
490
|
+
numFmt: columnNumFmt(col, columnStyles[col.field]),
|
|
327
491
|
}
|
|
328
492
|
}));
|
|
329
493
|
|
|
494
|
+
/** Every cell of a row, including empty ones (ExcelJS eachCell skips them). */
|
|
495
|
+
const forEachColumnCell = (row: import('exceljs').Row, fn: (cell: import('exceljs').Cell, colDef: GridColDef<GridRowModel>, colIndex: number) => void) => {
|
|
496
|
+
exportColumns.forEach((colDef, i) => fn(row.getCell(i + 1), colDef, i + 1));
|
|
497
|
+
};
|
|
498
|
+
|
|
330
499
|
// ── Header row ───────────────────────────────────────────────────────
|
|
331
500
|
if (includeHeaders) {
|
|
332
501
|
const headerValues = exportColumns.map(col => col.headerName ?? col.field);
|
|
333
502
|
const headerRow = ws.addRow(headerValues);
|
|
334
503
|
headerRow.height = 20;
|
|
335
504
|
|
|
336
|
-
headerRow
|
|
337
|
-
const colDef = exportColumns[colIndex - 1];
|
|
505
|
+
forEachColumnCell(headerRow, (cell, colDef) => {
|
|
338
506
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(headerFillColor) } };
|
|
339
507
|
cell.font = { bold: true, size: headerFontSize, color: { argb: argb(headerTextColor) } };
|
|
340
508
|
cell.alignment = {
|
|
341
|
-
horizontal: columnStyles[colDef
|
|
342
|
-
(colDef
|
|
509
|
+
horizontal: columnStyles[colDef.field]?.alignment ??
|
|
510
|
+
(colDef.type === 'number' ? 'right' : 'left'),
|
|
343
511
|
vertical: 'middle'
|
|
344
512
|
};
|
|
345
513
|
cell.border = bottomBorder();
|
|
@@ -368,33 +536,22 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
368
536
|
const cellValues = exportColumns.map(col => {
|
|
369
537
|
// Image columns: write empty string; image placed below
|
|
370
538
|
if (columnStyles[col.field]?.embedImage) return '';
|
|
371
|
-
|
|
372
|
-
let value: unknown = row[col.field];
|
|
373
|
-
if (col.valueGetter) {
|
|
374
|
-
value = col.valueGetter({ row, field: col.field, value });
|
|
375
|
-
}
|
|
376
|
-
const cellType = inferCellType(col, value);
|
|
377
|
-
if (cellType === 'string' && col.valueFormatter && value != null) {
|
|
378
|
-
value = col.valueFormatter({ value, row, field: col.field });
|
|
379
|
-
}
|
|
380
|
-
return value;
|
|
539
|
+
return dataCellValue(row, col);
|
|
381
540
|
});
|
|
382
541
|
|
|
383
542
|
const dataRow = ws.addRow(cellValues);
|
|
384
543
|
dataRow.font = { size: bodyFontSize };
|
|
385
544
|
if (outlineLevel > 0) dataRow.outlineLevel = outlineLevel;
|
|
386
545
|
|
|
387
|
-
|
|
388
|
-
if (alternateRowColor && rowIdx % 2 === 1) {
|
|
389
|
-
dataRow.eachCell(cell => {
|
|
390
|
-
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(alternateRowColor) } };
|
|
391
|
-
});
|
|
392
|
-
}
|
|
546
|
+
const striped = Boolean(alternateRowColor) && rowIdx % 2 === 1;
|
|
393
547
|
|
|
394
|
-
// Per-cell alignment and
|
|
395
|
-
dataRow
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
548
|
+
// Per-cell number format, stripe, alignment and web-like borders (empty cells included)
|
|
549
|
+
forEachColumnCell(dataRow, (cell, colDef, colIndex) => {
|
|
550
|
+
const numFmt = cellNumFmt(colDef, columnStyles[colDef.field], cellValues[colIndex - 1]);
|
|
551
|
+
if (numFmt) cell.numFmt = numFmt;
|
|
552
|
+
if (striped && alternateRowColor) {
|
|
553
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(alternateRowColor) } };
|
|
554
|
+
}
|
|
398
555
|
cell.alignment = {
|
|
399
556
|
horizontal: columnStyles[colDef.field]?.alignment ?? (colDef.type === 'number' ? 'right' : 'left'),
|
|
400
557
|
vertical: 'middle',
|
|
@@ -421,12 +578,12 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
421
578
|
|
|
422
579
|
imageColumns.forEach(col => {
|
|
423
580
|
const colIdx = exportColumns.indexOf(col);
|
|
424
|
-
const url = row
|
|
425
|
-
const imgId =
|
|
581
|
+
const url = imageUrlOf(row, col);
|
|
582
|
+
const imgId = url ? imageCache.get(url) : undefined;
|
|
426
583
|
|
|
427
584
|
if (imgId === undefined) {
|
|
428
|
-
// Fetch failed — write URL text so data isn't lost
|
|
429
|
-
dataRow.getCell(colIdx + 1).value =
|
|
585
|
+
// Fetch failed or unsupported format — write URL text so data isn't lost
|
|
586
|
+
dataRow.getCell(colIdx + 1).value = url ?? '';
|
|
430
587
|
return;
|
|
431
588
|
}
|
|
432
589
|
|
|
@@ -464,33 +621,19 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
464
621
|
excelRowIdx++;
|
|
465
622
|
};
|
|
466
623
|
|
|
467
|
-
const groupLabel = (entry: GridGroupedExportRow): string =>
|
|
468
|
-
const colDef = exportColumns.find(c => c.field === entry.groupField);
|
|
469
|
-
if (colDef?.groupingValueFormatter && entry.groupField) {
|
|
470
|
-
return colDef.groupingValueFormatter({ field: entry.groupField, value: entry.groupValue });
|
|
471
|
-
}
|
|
472
|
-
return `${colDef?.headerName ?? entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
// Numbers are written raw so the column numFmt applies; other values use the formatter.
|
|
476
|
-
const aggregateCell = (col: GridColDef<GridRowModel>, values: Record<string, unknown>): unknown => {
|
|
477
|
-
const val = values[col.field];
|
|
478
|
-
if (val == null) return '';
|
|
479
|
-
if (typeof val === 'number') return val;
|
|
480
|
-
if (col.valueFormatter) return col.valueFormatter({ value: val, row: {} as GridRowModel, field: col.field });
|
|
481
|
-
return formatAggregationValue(val, aggregationModel?.[col.field] ?? '');
|
|
482
|
-
};
|
|
624
|
+
const groupLabel = (entry: GridGroupedExportRow): string => groupHeaderLabel(entry, allColumns);
|
|
483
625
|
|
|
484
626
|
const writeSummaryRow = (label: string, values: Record<string, unknown>, fill: string, outlineLevel: number, bold: boolean) => {
|
|
485
|
-
const cells = exportColumns.map((col, i) => (i === 0 && values[col.field] == null ? label : aggregateCell(col, values)));
|
|
486
|
-
const summaryRow = ws.addRow(cells);
|
|
627
|
+
const cells = exportColumns.map((col, i): AggregateCell => (i === 0 && values[col.field] == null ? { value: label } : aggregateCell(col, values)));
|
|
628
|
+
const summaryRow = ws.addRow(cells.map(c => c.value));
|
|
487
629
|
summaryRow.height = 18;
|
|
488
630
|
if (outlineLevel > 0) summaryRow.outlineLevel = outlineLevel;
|
|
489
631
|
summaryRow.font = { bold, italic: !bold, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
|
|
490
|
-
summaryRow
|
|
632
|
+
forEachColumnCell(summaryRow, (cell, colDef, colIndex) => {
|
|
633
|
+
const numFmt = cells[colIndex - 1].numFmt;
|
|
634
|
+
if (numFmt) cell.numFmt = numFmt;
|
|
491
635
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(fill) } };
|
|
492
|
-
|
|
493
|
-
cell.alignment = { horizontal: colIndex === 1 ? 'left' : (colDef?.type === 'number' ? 'right' : 'left'), vertical: 'middle' };
|
|
636
|
+
cell.alignment = { horizontal: colIndex === 1 ? 'left' : (colDef.type === 'number' ? 'right' : 'left'), vertical: 'middle' };
|
|
494
637
|
cell.border = topBorder();
|
|
495
638
|
});
|
|
496
639
|
excelRowIdx++;
|
|
@@ -527,34 +670,30 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
527
670
|
}
|
|
528
671
|
|
|
529
672
|
// ── Aggregation totals row ────────────────────────────────────────────
|
|
530
|
-
|
|
673
|
+
// A 'selected' sheet's totals are recomputed over the rows it contains.
|
|
674
|
+
const sheetTotals = aggregationForExport(rowsToExport, rowScope === 'selected', allColumns, aggregationResult, aggregationModel);
|
|
675
|
+
if (includeSummary && !wroteGrandTotal && sheetTotals && aggregationModel) {
|
|
531
676
|
// Label row
|
|
532
677
|
const labelValues = exportColumns.map(col => {
|
|
533
|
-
const fn = aggregationModel
|
|
678
|
+
const fn = aggregationModel[col.field];
|
|
534
679
|
return fn ? fn.toUpperCase() : '';
|
|
535
680
|
});
|
|
536
681
|
const labelRow = ws.addRow(labelValues);
|
|
537
682
|
labelRow.height = 16;
|
|
538
683
|
labelRow.font = { size: bodyFontSize - 1, italic: true, color: { argb: 'FF94a3b8' } };
|
|
539
|
-
labelRow
|
|
684
|
+
forEachColumnCell(labelRow, cell => {
|
|
540
685
|
cell.alignment = { horizontal: 'right', vertical: 'middle' };
|
|
541
686
|
cell.border = { top: { style: 'thin', color: { argb: 'FFcbd5e1' } } };
|
|
542
687
|
});
|
|
543
688
|
|
|
544
689
|
// Value row
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
const val = aggregationResult![col.field];
|
|
548
|
-
if (fn == null || val == null) return '';
|
|
549
|
-
if (col.valueFormatter) {
|
|
550
|
-
return col.valueFormatter({ value: val, row: {} as R, field: col.field });
|
|
551
|
-
}
|
|
552
|
-
return formatAggregationValue(val, fn);
|
|
553
|
-
});
|
|
554
|
-
const totalRow = ws.addRow(totalValues);
|
|
690
|
+
const totals = exportColumns.map((col): AggregateCell => (aggregationModel[col.field] == null ? { value: null } : aggregateCell(col, sheetTotals)));
|
|
691
|
+
const totalRow = ws.addRow(totals.map(t => t.value));
|
|
555
692
|
totalRow.height = 18;
|
|
556
693
|
totalRow.font = { bold: true, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
|
|
557
|
-
totalRow
|
|
694
|
+
forEachColumnCell(totalRow, (cell, _colDef, colIndex) => {
|
|
695
|
+
const numFmt = totals[colIndex - 1].numFmt;
|
|
696
|
+
if (numFmt) cell.numFmt = numFmt;
|
|
558
697
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(headerFillColor) } };
|
|
559
698
|
cell.alignment = { horizontal: 'right', vertical: 'middle' };
|
|
560
699
|
cell.border = topBorder();
|