@opencorestack/opengridx 2.0.4 → 3.0.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/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- 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 -161
- 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 +45 -22
- package/docs/components/datagrid.md +14 -12
- 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 +100 -36
- 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 +147 -41
- 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 +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -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 +11 -0
- 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 +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- 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 +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- 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 +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- 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 +15 -3
- 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 +284 -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 +123 -0
- 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 +452 -250
- 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 +20 -7
- 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 +86 -90
- 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 +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -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 +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- 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 +344 -121
- 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 +78 -0
- 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 +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- 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/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 -137
- 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 } 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,13 +81,43 @@ 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;
|
|
91
|
+
/**
|
|
92
|
+
* Grouped row structure, typically from `apiRef.current.getGroupedExportRows()`.
|
|
93
|
+
* When provided, sheets with `rows: 'all'` are written in grouped order: group-header,
|
|
94
|
+
* leaf, group-subtotal and grand-total rows, using Excel row outlining so groups
|
|
95
|
+
* collapse natively. A `grand-total` entry replaces the sheet's `includeSummary` row.
|
|
96
|
+
* Ignored for `rows: 'selected'` sheets.
|
|
97
|
+
* @since v2.1
|
|
98
|
+
*/
|
|
99
|
+
groupedRows?: GridGroupedExportRow[];
|
|
100
|
+
/** Fill color for group-header rows in grouped export (default: '#e8eaf6') */
|
|
101
|
+
groupHeaderFillColor?: string;
|
|
102
|
+
/** Fill color for group-subtotal rows in grouped export (default: '#f0f4ff') */
|
|
103
|
+
groupSubtotalFillColor?: string;
|
|
71
104
|
}
|
|
72
105
|
|
|
73
|
-
// ─── 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
|
+
}
|
|
74
117
|
|
|
75
|
-
|
|
118
|
+
type CellKind = 'number' | 'boolean' | 'date' | 'string';
|
|
119
|
+
|
|
120
|
+
function cellKind(colDef: GridColDef<GridRowModel>, value: unknown): CellKind {
|
|
76
121
|
if (colDef.type === 'number') return 'number';
|
|
77
122
|
if (colDef.type === 'boolean') return 'boolean';
|
|
78
123
|
if (colDef.type === 'date') return 'date';
|
|
@@ -82,14 +127,131 @@ function inferCellType(colDef: GridColDef<GridRowModel>, value: unknown): 'numbe
|
|
|
82
127
|
return 'string';
|
|
83
128
|
}
|
|
84
129
|
|
|
85
|
-
|
|
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 ──────────────────────────────────────────────────────────
|
|
86
231
|
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
245
|
if (colDef.type === 'date') return 'yyyy-mm-dd';
|
|
90
246
|
return undefined;
|
|
91
247
|
}
|
|
92
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
|
+
|
|
93
255
|
// ─── Column display width (chars) ────────────────────────────────────────────
|
|
94
256
|
|
|
95
257
|
function colWidth(colDef: GridColDef<GridRowModel>, override?: number): number {
|
|
@@ -102,48 +264,49 @@ function colWidth(colDef: GridColDef<GridRowModel>, override?: number): number {
|
|
|
102
264
|
|
|
103
265
|
type SupportedImageExt = 'png' | 'jpeg' | 'gif';
|
|
104
266
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
if (
|
|
112
|
-
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;
|
|
113
282
|
}
|
|
114
283
|
|
|
115
284
|
/**
|
|
116
285
|
* Fetch a list of image URLs in parallel.
|
|
117
|
-
* CORS failures are logged and skipped — raw URL text is written to the
|
|
118
|
-
* 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.
|
|
119
288
|
*/
|
|
120
289
|
async function fetchImages(
|
|
121
290
|
urls: string[]
|
|
122
291
|
): Promise<Map<string, { buffer: ArrayBuffer; extension: SupportedImageExt }>> {
|
|
123
292
|
const settled = await Promise.allSettled(
|
|
124
293
|
urls.map(async (url) => {
|
|
125
|
-
|
|
294
|
+
let bytes: Uint8Array;
|
|
126
295
|
if (url.startsWith('data:')) {
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
else if (mime.includes('gif')) ext = 'gif';
|
|
138
|
-
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());
|
|
139
306
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
144
|
-
const buffer = await res.arrayBuffer();
|
|
145
|
-
const ext = imageExtension(url, res.headers.get('content-type'));
|
|
146
|
-
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 };
|
|
147
310
|
})
|
|
148
311
|
);
|
|
149
312
|
const map = new Map<string, { buffer: ArrayBuffer; extension: SupportedImageExt }>();
|
|
@@ -153,7 +316,7 @@ async function fetchImages(
|
|
|
153
316
|
} else {
|
|
154
317
|
console.warn(
|
|
155
318
|
`[exportToExcelAdvanced] Cannot embed image "${urls[i]}" ` +
|
|
156
|
-
`(CORS/network error — raw URL written as fallback):`,
|
|
319
|
+
`(CORS/network error or unsupported format — raw URL written as fallback):`,
|
|
157
320
|
result.reason
|
|
158
321
|
);
|
|
159
322
|
}
|
|
@@ -171,6 +334,8 @@ async function fetchImages(
|
|
|
171
334
|
* ExcelJS is loaded lazily so it does not affect bundle size for apps
|
|
172
335
|
* that only use CSV/JSON export.
|
|
173
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>;
|
|
174
339
|
export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
175
340
|
rows: R[],
|
|
176
341
|
columns: GridColDef<R>[],
|
|
@@ -187,6 +352,10 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
187
352
|
aggregationResult = null,
|
|
188
353
|
aggregationModel = null,
|
|
189
354
|
selectedRows,
|
|
355
|
+
getRowId,
|
|
356
|
+
groupedRows,
|
|
357
|
+
groupHeaderFillColor = '#e8eaf6',
|
|
358
|
+
groupSubtotalFillColor = '#f0f4ff',
|
|
190
359
|
} = options;
|
|
191
360
|
|
|
192
361
|
// Lazy-load ExcelJS to keep initial bundle lean
|
|
@@ -195,26 +364,40 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
195
364
|
workbook.creator = 'OpenGridX';
|
|
196
365
|
workbook.created = new Date();
|
|
197
366
|
|
|
198
|
-
|
|
199
|
-
const exportColumns: GridColDef<GridRowModel>[] = (
|
|
200
|
-
col.exportable !== false &&
|
|
201
|
-
col.field !== '__check__' &&
|
|
202
|
-
col.field !== '__actions__' &&
|
|
203
|
-
!col.isSpacer
|
|
204
|
-
);
|
|
367
|
+
const allColumns = columns as unknown as GridColDef<GridRowModel>[];
|
|
368
|
+
const exportColumns: GridColDef<GridRowModel>[] = getExportColumns(allColumns);
|
|
205
369
|
|
|
206
370
|
const allRows: GridRowModel[] = rows as GridRowModel[];
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
: 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) : [];
|
|
210
373
|
|
|
211
374
|
// Normalise sheet definitions
|
|
212
375
|
const resolvedSheets: (ExcelSheetDefinition | { type: 'summary'; name?: string })[] =
|
|
213
376
|
sheetDefs ?? [{ name: 'Data', rows: 'all', includeHeaders: true, includeSummary: false }];
|
|
377
|
+
const usedSheetNames = new Set<string>();
|
|
214
378
|
|
|
215
379
|
// Workbook-level image cache — one fetch per URL, reused across multiple sheets
|
|
216
380
|
const imageCache = new Map<string, number>();
|
|
217
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
|
+
|
|
218
401
|
// ─── Build each sheet ────────────────────────────────────────────────────
|
|
219
402
|
|
|
220
403
|
for (const sheetDef of resolvedSheets) {
|
|
@@ -222,7 +405,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
222
405
|
// ── Summary-only sheet ───────────────────────────────────────────────
|
|
223
406
|
if ('type' in sheetDef && sheetDef.type === 'summary') {
|
|
224
407
|
if (!aggregationResult || !aggregationModel) continue;
|
|
225
|
-
const ws = workbook.addWorksheet(sheetDef.name ?? 'Summary');
|
|
408
|
+
const ws = workbook.addWorksheet(sanitizeSheetName(sheetDef.name ?? 'Summary', usedSheetNames));
|
|
226
409
|
|
|
227
410
|
// Title row
|
|
228
411
|
ws.addRow(['Summary / Aggregation Totals']);
|
|
@@ -238,18 +421,15 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
238
421
|
});
|
|
239
422
|
|
|
240
423
|
exportColumns.forEach(col => {
|
|
241
|
-
const fn = aggregationModel
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
const row = ws.addRow([col.headerName ?? col.field, fn.toUpperCase(), formatted]);
|
|
251
|
-
row.getCell(3).alignment = { horizontal: 'right' };
|
|
252
|
-
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 };
|
|
253
433
|
});
|
|
254
434
|
|
|
255
435
|
ws.getColumn(1).width = 20;
|
|
@@ -270,7 +450,12 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
270
450
|
alternateRowColor = '#f8fafc',
|
|
271
451
|
} = def;
|
|
272
452
|
|
|
273
|
-
const
|
|
453
|
+
const useGrouped = rowScope === 'all' && Boolean(groupedRows && groupedRows.length > 0);
|
|
454
|
+
const rowsToExport = rowScope === 'selected'
|
|
455
|
+
? sRows
|
|
456
|
+
: useGrouped
|
|
457
|
+
? groupedRows!.flatMap(e => (e.type === 'leaf' && e.row ? [e.row] : []))
|
|
458
|
+
: allRows;
|
|
274
459
|
|
|
275
460
|
// ─ Pre-fetch images for this sheet ──────────────────────────────────────────
|
|
276
461
|
const imageColumns = exportColumns.filter(col => columnStyles[col.field]?.embedImage);
|
|
@@ -279,10 +464,10 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
279
464
|
const seen = new Set<string>();
|
|
280
465
|
rowsToExport.forEach(row => {
|
|
281
466
|
imageColumns.forEach(col => {
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
284
|
-
urlsNeeded.push(
|
|
285
|
-
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);
|
|
286
471
|
}
|
|
287
472
|
});
|
|
288
473
|
});
|
|
@@ -295,30 +480,34 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
295
480
|
}
|
|
296
481
|
}
|
|
297
482
|
|
|
298
|
-
const ws = workbook.addWorksheet(name);
|
|
483
|
+
const ws = workbook.addWorksheet(sanitizeSheetName(name, usedSheetNames));
|
|
299
484
|
|
|
300
485
|
// ── Set column definitions ───────────────────────────────────────────
|
|
301
486
|
ws.columns = exportColumns.map(col => ({
|
|
302
487
|
key: col.field,
|
|
303
488
|
width: colWidth(col, columnStyles[col.field]?.width),
|
|
304
489
|
style: {
|
|
305
|
-
numFmt: columnStyles[col.field]
|
|
490
|
+
numFmt: columnNumFmt(col, columnStyles[col.field]),
|
|
306
491
|
}
|
|
307
492
|
}));
|
|
308
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
|
+
|
|
309
499
|
// ── Header row ───────────────────────────────────────────────────────
|
|
310
500
|
if (includeHeaders) {
|
|
311
501
|
const headerValues = exportColumns.map(col => col.headerName ?? col.field);
|
|
312
502
|
const headerRow = ws.addRow(headerValues);
|
|
313
503
|
headerRow.height = 20;
|
|
314
504
|
|
|
315
|
-
headerRow
|
|
316
|
-
const colDef = exportColumns[colIndex - 1];
|
|
505
|
+
forEachColumnCell(headerRow, (cell, colDef) => {
|
|
317
506
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(headerFillColor) } };
|
|
318
507
|
cell.font = { bold: true, size: headerFontSize, color: { argb: argb(headerTextColor) } };
|
|
319
508
|
cell.alignment = {
|
|
320
|
-
horizontal: columnStyles[colDef
|
|
321
|
-
(colDef
|
|
509
|
+
horizontal: columnStyles[colDef.field]?.alignment ??
|
|
510
|
+
(colDef.type === 'number' ? 'right' : 'left'),
|
|
322
511
|
vertical: 'middle'
|
|
323
512
|
};
|
|
324
513
|
cell.border = bottomBorder();
|
|
@@ -343,36 +532,26 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
343
532
|
// Header occupies index 0 when present; first data row is at index 1.
|
|
344
533
|
let excelRowIdx = includeHeaders ? 1 : 0;
|
|
345
534
|
|
|
346
|
-
|
|
535
|
+
const writeDataRow = (row: GridRowModel, rowIdx: number, outlineLevel: number) => {
|
|
347
536
|
const cellValues = exportColumns.map(col => {
|
|
348
537
|
// Image columns: write empty string; image placed below
|
|
349
538
|
if (columnStyles[col.field]?.embedImage) return '';
|
|
350
|
-
|
|
351
|
-
let value: unknown = row[col.field];
|
|
352
|
-
if (col.valueGetter) {
|
|
353
|
-
value = col.valueGetter({ row, field: col.field, value });
|
|
354
|
-
}
|
|
355
|
-
const cellType = inferCellType(col, value);
|
|
356
|
-
if (cellType === 'string' && col.valueFormatter && value != null) {
|
|
357
|
-
value = col.valueFormatter({ value, row, field: col.field });
|
|
358
|
-
}
|
|
359
|
-
return value;
|
|
539
|
+
return dataCellValue(row, col);
|
|
360
540
|
});
|
|
361
541
|
|
|
362
542
|
const dataRow = ws.addRow(cellValues);
|
|
363
543
|
dataRow.font = { size: bodyFontSize };
|
|
544
|
+
if (outlineLevel > 0) dataRow.outlineLevel = outlineLevel;
|
|
364
545
|
|
|
365
|
-
|
|
366
|
-
if (alternateRowColor && rowIdx % 2 === 1) {
|
|
367
|
-
dataRow.eachCell(cell => {
|
|
368
|
-
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(alternateRowColor) } };
|
|
369
|
-
});
|
|
370
|
-
}
|
|
546
|
+
const striped = Boolean(alternateRowColor) && rowIdx % 2 === 1;
|
|
371
547
|
|
|
372
|
-
// Per-cell alignment and
|
|
373
|
-
dataRow
|
|
374
|
-
const
|
|
375
|
-
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
|
+
}
|
|
376
555
|
cell.alignment = {
|
|
377
556
|
horizontal: columnStyles[colDef.field]?.alignment ?? (colDef.type === 'number' ? 'right' : 'left'),
|
|
378
557
|
vertical: 'middle',
|
|
@@ -399,12 +578,12 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
399
578
|
|
|
400
579
|
imageColumns.forEach(col => {
|
|
401
580
|
const colIdx = exportColumns.indexOf(col);
|
|
402
|
-
const url = row
|
|
403
|
-
const imgId =
|
|
581
|
+
const url = imageUrlOf(row, col);
|
|
582
|
+
const imgId = url ? imageCache.get(url) : undefined;
|
|
404
583
|
|
|
405
584
|
if (imgId === undefined) {
|
|
406
|
-
// Fetch failed — write URL text so data isn't lost
|
|
407
|
-
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 ?? '';
|
|
408
587
|
return;
|
|
409
588
|
}
|
|
410
589
|
|
|
@@ -440,37 +619,81 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
|
|
|
440
619
|
}
|
|
441
620
|
|
|
442
621
|
excelRowIdx++;
|
|
443
|
-
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const groupLabel = (entry: GridGroupedExportRow): string => groupHeaderLabel(entry, allColumns);
|
|
625
|
+
|
|
626
|
+
const writeSummaryRow = (label: string, values: Record<string, unknown>, fill: string, outlineLevel: number, bold: boolean) => {
|
|
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));
|
|
629
|
+
summaryRow.height = 18;
|
|
630
|
+
if (outlineLevel > 0) summaryRow.outlineLevel = outlineLevel;
|
|
631
|
+
summaryRow.font = { bold, italic: !bold, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
|
|
632
|
+
forEachColumnCell(summaryRow, (cell, colDef, colIndex) => {
|
|
633
|
+
const numFmt = cells[colIndex - 1].numFmt;
|
|
634
|
+
if (numFmt) cell.numFmt = numFmt;
|
|
635
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(fill) } };
|
|
636
|
+
cell.alignment = { horizontal: colIndex === 1 ? 'left' : (colDef.type === 'number' ? 'right' : 'left'), vertical: 'middle' };
|
|
637
|
+
cell.border = topBorder();
|
|
638
|
+
});
|
|
639
|
+
excelRowIdx++;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
let wroteGrandTotal = false;
|
|
643
|
+
|
|
644
|
+
if (useGrouped) {
|
|
645
|
+
let stripe = 0;
|
|
646
|
+
groupedRows!.forEach(entry => {
|
|
647
|
+
if (entry.type === 'group-header') {
|
|
648
|
+
const groupHeaderRow = ws.addRow([groupLabel(entry)]);
|
|
649
|
+
groupHeaderRow.height = 18;
|
|
650
|
+
if (entry.depth > 0) groupHeaderRow.outlineLevel = entry.depth;
|
|
651
|
+
groupHeaderRow.font = { bold: true, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
|
|
652
|
+
for (let c = 1; c <= exportColumns.length; c++) {
|
|
653
|
+
groupHeaderRow.getCell(c).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(groupHeaderFillColor) } };
|
|
654
|
+
}
|
|
655
|
+
groupHeaderRow.getCell(1).alignment = { horizontal: 'left', vertical: 'middle', indent: entry.depth };
|
|
656
|
+
excelRowIdx++;
|
|
657
|
+
stripe = 0;
|
|
658
|
+
} else if (entry.type === 'leaf' && entry.row) {
|
|
659
|
+
writeDataRow(entry.row, stripe++, entry.depth);
|
|
660
|
+
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
|
|
661
|
+
writeSummaryRow('Subtotal', entry.aggregatedValues, groupSubtotalFillColor, entry.depth, false);
|
|
662
|
+
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
663
|
+
writeSummaryRow('Grand Total', entry.aggregatedValues, headerFillColor, 0, true);
|
|
664
|
+
wroteGrandTotal = true;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
ws.properties.outlineProperties = { summaryBelow: true, summaryRight: false };
|
|
668
|
+
} else {
|
|
669
|
+
rowsToExport.forEach((row, rowIdx) => writeDataRow(row, rowIdx, 0));
|
|
670
|
+
}
|
|
444
671
|
|
|
445
672
|
// ── Aggregation totals row ────────────────────────────────────────────
|
|
446
|
-
|
|
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) {
|
|
447
676
|
// Label row
|
|
448
677
|
const labelValues = exportColumns.map(col => {
|
|
449
|
-
const fn = aggregationModel
|
|
678
|
+
const fn = aggregationModel[col.field];
|
|
450
679
|
return fn ? fn.toUpperCase() : '';
|
|
451
680
|
});
|
|
452
681
|
const labelRow = ws.addRow(labelValues);
|
|
453
682
|
labelRow.height = 16;
|
|
454
683
|
labelRow.font = { size: bodyFontSize - 1, italic: true, color: { argb: 'FF94a3b8' } };
|
|
455
|
-
labelRow
|
|
684
|
+
forEachColumnCell(labelRow, cell => {
|
|
456
685
|
cell.alignment = { horizontal: 'right', vertical: 'middle' };
|
|
457
686
|
cell.border = { top: { style: 'thin', color: { argb: 'FFcbd5e1' } } };
|
|
458
687
|
});
|
|
459
688
|
|
|
460
689
|
// Value row
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
const val = aggregationResult![col.field];
|
|
464
|
-
if (fn == null || val == null) return '';
|
|
465
|
-
if (col.valueFormatter) {
|
|
466
|
-
return col.valueFormatter({ value: val, row: {} as R, field: col.field });
|
|
467
|
-
}
|
|
468
|
-
return formatAggregationValue(val, fn);
|
|
469
|
-
});
|
|
470
|
-
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));
|
|
471
692
|
totalRow.height = 18;
|
|
472
693
|
totalRow.font = { bold: true, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
|
|
473
|
-
totalRow
|
|
694
|
+
forEachColumnCell(totalRow, (cell, _colDef, colIndex) => {
|
|
695
|
+
const numFmt = totals[colIndex - 1].numFmt;
|
|
696
|
+
if (numFmt) cell.numFmt = numFmt;
|
|
474
697
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(headerFillColor) } };
|
|
475
698
|
cell.alignment = { horizontal: 'right', vertical: 'middle' };
|
|
476
699
|
cell.border = topBorder();
|