@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,18 +1,36 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
GridColDef,
|
|
3
3
|
GridRowModel,
|
|
4
|
+
GridValidRowModel,
|
|
4
5
|
GridFilterModel,
|
|
6
|
+
GridFilterGroup,
|
|
7
|
+
GridFilterItem,
|
|
5
8
|
PdfExportOptions,
|
|
6
9
|
GridGroupedExportRow,
|
|
7
10
|
} from '../../types';
|
|
8
|
-
import {
|
|
11
|
+
import { groupHeaderLabel } from './groupLabel';
|
|
12
|
+
import {
|
|
13
|
+
aggregationForExport,
|
|
14
|
+
formatExportAggregate,
|
|
15
|
+
formatExportValue,
|
|
16
|
+
getExportColumns,
|
|
17
|
+
getRawExportValue,
|
|
18
|
+
hasSelection,
|
|
19
|
+
rowsForExport,
|
|
20
|
+
shouldExportGrouped,
|
|
21
|
+
summaryLabelText,
|
|
22
|
+
} from './exportShared';
|
|
23
|
+
import { toWinAnsi } from './pdfText';
|
|
9
24
|
|
|
10
25
|
interface JsPDFDoc {
|
|
11
26
|
save: (filename: string) => void;
|
|
12
27
|
setFontSize: (size: number) => void;
|
|
13
28
|
setFont: (name: string, style: string) => void;
|
|
14
29
|
setTextColor: (r: number, g: number, b: number) => void;
|
|
15
|
-
text: (text: string, x: number, y: number, opts?: { align?: string }) => void;
|
|
30
|
+
text: (text: string | string[], x: number, y: number, opts?: { align?: string }) => void;
|
|
31
|
+
splitTextToSize: (text: string, maxWidth: number) => string[];
|
|
32
|
+
addFileToVFS: (fileName: string, data: string) => void;
|
|
33
|
+
addFont: (fileName: string, fontName: string, fontStyle: string) => void;
|
|
16
34
|
addImage: (data: string, format: string, x: number, y: number, w: number, h: number) => void;
|
|
17
35
|
line: (x1: number, y1: number, x2: number, y2: number) => void;
|
|
18
36
|
setDrawColor: (r: number, g: number, b: number) => void;
|
|
@@ -41,33 +59,22 @@ interface AutoTableOptions {
|
|
|
41
59
|
margin?: { top: number; left: number; right: number; bottom: number };
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
46
|
-
if (!result) return [79, 70, 229];
|
|
47
|
-
return [
|
|
48
|
-
parseInt(result[1], 16),
|
|
49
|
-
parseInt(result[2], 16),
|
|
50
|
-
parseInt(result[3], 16),
|
|
51
|
-
];
|
|
52
|
-
}
|
|
62
|
+
type Rgb = [number, number, number];
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
const DEFAULT_HEADER_BACKGROUND: Rgb = [79, 70, 229];
|
|
65
|
+
const DEFAULT_HEADER_TEXT: Rgb = [255, 255, 255];
|
|
66
|
+
|
|
67
|
+
/** '#rrggbb' or '#rgb' as RGB; invalid input gives `fallback` (the option's own default). */
|
|
68
|
+
function hexToRgb(hex: string, fallback: Rgb): Rgb {
|
|
69
|
+
const clean = hex.trim().replace(/^#/, '');
|
|
70
|
+
const full = /^[a-f\d]{3}$/i.test(clean) ? clean.split('').map(c => c + c).join('') : clean;
|
|
71
|
+
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(full);
|
|
72
|
+
if (!result) return fallback;
|
|
73
|
+
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
function resolveValue<R extends GridRowModel>(row: R, col: GridColDef<R>): string {
|
|
63
|
-
|
|
64
|
-
if (col.valueGetter) {
|
|
65
|
-
value = col.valueGetter({ row, field: col.field, value });
|
|
66
|
-
}
|
|
67
|
-
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
68
|
-
value = col.valueFormatter({ value, row, field: col.field });
|
|
69
|
-
}
|
|
70
|
-
return String(value ?? '');
|
|
77
|
+
return formatExportValue(row, col, getRawExportValue(row, col));
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
function colWeight<R extends GridRowModel>(col: GridColDef<R>): number {
|
|
@@ -87,27 +94,60 @@ function computeColumnStyles<R extends GridRowModel>(
|
|
|
87
94
|
return styles;
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
const NO_VALUE_OPERATORS = new Set(['isEmpty', 'isNotEmpty']);
|
|
98
|
+
|
|
99
|
+
function describeFilterItem<R extends GridRowModel>(item: GridFilterItem, columns: GridColDef<R>[]): string | null {
|
|
100
|
+
const col = columns.find(c => c.field === item.field);
|
|
101
|
+
const label = col?.headerName ?? item.field;
|
|
102
|
+
if (NO_VALUE_OPERATORS.has(item.operator)) return `${label} ${item.operator}`;
|
|
103
|
+
const { value } = item;
|
|
104
|
+
// An item without a value is an unfinished rule in the filter panel, not a real condition.
|
|
105
|
+
if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) return null;
|
|
106
|
+
const text = Array.isArray(value) ? value.map(String).join(', ') : String(value);
|
|
107
|
+
return `${label} ${item.operator} "${text}"`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeFilterItems<R extends GridRowModel>(
|
|
111
|
+
items: (GridFilterItem | GridFilterGroup)[],
|
|
112
|
+
logicOperator: 'and' | 'or',
|
|
113
|
+
columns: GridColDef<R>[],
|
|
114
|
+
nested: boolean,
|
|
115
|
+
): string {
|
|
116
|
+
const parts = items
|
|
117
|
+
.map(item => ('items' in item
|
|
118
|
+
? describeFilterItems(item.items, item.logicOperator, columns, true)
|
|
119
|
+
: describeFilterItem(item, columns)))
|
|
120
|
+
.filter((part): part is string => !!part);
|
|
121
|
+
if (parts.length === 0) return '';
|
|
122
|
+
const joined = parts.join(logicOperator === 'or' ? ' OR ' : ' AND ');
|
|
123
|
+
return nested && parts.length > 1 ? `(${joined})` : joined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The applied filter as text: nested groups in parentheses, joined with their logic operator,
|
|
128
|
+
* plus the quick-search terms.
|
|
129
|
+
*/
|
|
90
130
|
function formatFilterSummary<R extends GridRowModel>(
|
|
91
131
|
filterModel: GridFilterModel,
|
|
92
132
|
columns: GridColDef<R>[]
|
|
93
133
|
): string {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const label = col?.headerName ?? item.field;
|
|
101
|
-
const val = item.value !== undefined && item.value !== null ? String(item.value) : '';
|
|
102
|
-
return [`${label} ${item.operator} "${val}"`];
|
|
103
|
-
})
|
|
104
|
-
.join(' • ');
|
|
134
|
+
const parts: string[] = [];
|
|
135
|
+
const items = describeFilterItems(filterModel.items ?? [], filterModel.logicOperator ?? 'and', columns, false);
|
|
136
|
+
if (items) parts.push(items);
|
|
137
|
+
const search = (filterModel.quickFilterValues ?? []).filter(v => v.trim() !== '');
|
|
138
|
+
if (search.length > 0) parts.push(`Search: ${search.map(v => `"${v}"`).join(' ')}`);
|
|
139
|
+
return parts.join(' • ');
|
|
105
140
|
}
|
|
106
141
|
|
|
142
|
+
/** jsPDF's default line height factor, in mm per point of font size. */
|
|
143
|
+
const LINE_HEIGHT_MM_PER_PT = 1.15 * 0.3528;
|
|
144
|
+
|
|
145
|
+
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: PdfExportOptions<R>): Promise<void>;
|
|
146
|
+
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: PdfExportOptions<R>): Promise<void>;
|
|
107
147
|
export async function exportToPdf<R extends GridRowModel>(
|
|
108
148
|
rows: R[],
|
|
109
149
|
columns: GridColDef<R>[],
|
|
110
|
-
options: PdfExportOptions = {}
|
|
150
|
+
options: PdfExportOptions<R> = {}
|
|
111
151
|
): Promise<void> {
|
|
112
152
|
// Lazy-load peer deps — provides a clear error if not installed
|
|
113
153
|
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
@@ -131,6 +171,7 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
131
171
|
logoUrl,
|
|
132
172
|
orientation = 'landscape',
|
|
133
173
|
selectedRows,
|
|
174
|
+
getRowId,
|
|
134
175
|
aggregationResult,
|
|
135
176
|
aggregationModel,
|
|
136
177
|
filterModel,
|
|
@@ -139,6 +180,7 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
139
180
|
headerTextColor = '#ffffff',
|
|
140
181
|
fontSize = 9,
|
|
141
182
|
groupedRows,
|
|
183
|
+
font,
|
|
142
184
|
} = options;
|
|
143
185
|
|
|
144
186
|
const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
|
|
@@ -147,10 +189,33 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
147
189
|
const MARGIN = 14;
|
|
148
190
|
const usableWidth = pageWidth - MARGIN * 2;
|
|
149
191
|
|
|
192
|
+
// --- Font ---
|
|
193
|
+
let fontName = 'helvetica';
|
|
194
|
+
if (font) {
|
|
195
|
+
doc.addFileToVFS(`${font.name}-normal.ttf`, font.data);
|
|
196
|
+
doc.addFont(`${font.name}-normal.ttf`, font.name, 'normal');
|
|
197
|
+
if (font.boldData) doc.addFileToVFS(`${font.name}-bold.ttf`, font.boldData);
|
|
198
|
+
doc.addFont(font.boldData ? `${font.name}-bold.ttf` : `${font.name}-normal.ttf`, font.name, 'bold');
|
|
199
|
+
fontName = font.name;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Helvetica cannot draw text outside WinAnsi; keep what it can and replace the rest.
|
|
203
|
+
let replacedCharacters = false;
|
|
204
|
+
const pdfText = (text: string): string => {
|
|
205
|
+
if (font) return text;
|
|
206
|
+
const converted = toWinAnsi(text);
|
|
207
|
+
if (converted.lossy) replacedCharacters = true;
|
|
208
|
+
return converted.text;
|
|
209
|
+
};
|
|
210
|
+
const drawWrapped = (text: string, x: number, y: number, maxWidth: number, sizePt: number): number => {
|
|
211
|
+
const lines = doc.splitTextToSize(pdfText(text), maxWidth);
|
|
212
|
+
doc.text(lines.length === 1 ? lines[0] : lines, x, y);
|
|
213
|
+
return Math.max(0, lines.length - 1) * sizePt * LINE_HEIGHT_MM_PER_PT;
|
|
214
|
+
};
|
|
215
|
+
|
|
150
216
|
const exportColumns = getExportColumns(columns);
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
: rows;
|
|
217
|
+
const useGrouped = shouldExportGrouped(groupedRows, selectedRows);
|
|
218
|
+
const rowsToExport = rowsForExport(rows, selectedRows, getRowId);
|
|
154
219
|
|
|
155
220
|
// --- Header block ---
|
|
156
221
|
let startY = MARGIN;
|
|
@@ -167,22 +232,23 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
167
232
|
// logo failed to load — skip silently
|
|
168
233
|
}
|
|
169
234
|
}
|
|
235
|
+
const textWidth = pageWidth - MARGIN - cursorX;
|
|
170
236
|
|
|
171
237
|
doc.setFontSize(16);
|
|
172
|
-
doc.setFont(
|
|
238
|
+
doc.setFont(fontName, 'bold');
|
|
173
239
|
doc.setTextColor(30, 41, 59);
|
|
174
|
-
|
|
240
|
+
cursorY += drawWrapped(title, cursorX, cursorY, textWidth, 16);
|
|
175
241
|
cursorY += 6;
|
|
176
242
|
|
|
177
243
|
doc.setFontSize(8);
|
|
178
|
-
doc.setFont(
|
|
244
|
+
doc.setFont(fontName, 'normal');
|
|
179
245
|
doc.setTextColor(100, 116, 139);
|
|
180
246
|
const exportedAt = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
181
|
-
const rowCount =
|
|
247
|
+
const rowCount = useGrouped
|
|
182
248
|
? groupedRows.filter(e => e.type === 'leaf').length
|
|
183
249
|
: rowsToExport.length;
|
|
184
250
|
doc.text(
|
|
185
|
-
`Exported: ${exportedAt} • ${rowCount} row${rowCount !== 1 ? 's' : ''}
|
|
251
|
+
pdfText(`Exported: ${exportedAt} • ${rowCount} row${rowCount !== 1 ? 's' : ''}`),
|
|
186
252
|
cursorX,
|
|
187
253
|
cursorY
|
|
188
254
|
);
|
|
@@ -191,7 +257,7 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
191
257
|
if (filterModel) {
|
|
192
258
|
const summary = formatFilterSummary(filterModel, columns);
|
|
193
259
|
if (summary) {
|
|
194
|
-
|
|
260
|
+
cursorY += drawWrapped(`Filters: ${summary}`, cursorX, cursorY, textWidth, 8);
|
|
195
261
|
cursorY += 5;
|
|
196
262
|
}
|
|
197
263
|
}
|
|
@@ -203,83 +269,68 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
203
269
|
}
|
|
204
270
|
|
|
205
271
|
// --- Build table data ---
|
|
206
|
-
const head = [exportColumns.map(col => col.headerName ?? col.field)];
|
|
272
|
+
const head = [exportColumns.map(col => pdfText(col.headerName ?? col.field))];
|
|
273
|
+
const aggMod = aggregationModel || {};
|
|
274
|
+
const aggregateTexts = (values: Record<string, unknown>): string[] =>
|
|
275
|
+
exportColumns.map(col => formatExportAggregate(col, aggMod[col.field], values[col.field], values));
|
|
207
276
|
|
|
208
277
|
let body: string[][];
|
|
209
278
|
let foot: string[][] | undefined;
|
|
210
279
|
|
|
211
|
-
if (
|
|
280
|
+
if (useGrouped) {
|
|
212
281
|
// Grouped export: flatten the ordered list into body rows with indentation
|
|
213
282
|
body = [];
|
|
214
|
-
const aggMod = aggregationModel || {};
|
|
215
283
|
groupedRows.forEach((entry: GridGroupedExportRow) => {
|
|
284
|
+
const indent = ' '.repeat(entry.depth * 2);
|
|
216
285
|
if (entry.type === 'group-header') {
|
|
217
|
-
const
|
|
218
|
-
const label = `${indent}${entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
|
|
286
|
+
const label = `${indent}${groupHeaderLabel(entry, columns)}`;
|
|
219
287
|
body.push(exportColumns.map((_, i) => i === 0 ? label : ''));
|
|
220
288
|
} else if (entry.type === 'leaf' && entry.row) {
|
|
221
289
|
const row = entry.row as R;
|
|
222
|
-
const indent = ' '.repeat(entry.depth * 2);
|
|
223
290
|
body.push(exportColumns.map((col, i) => (i === 0 ? indent : '') + resolveValue(row, col)));
|
|
224
291
|
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
|
|
225
|
-
const
|
|
226
|
-
body.push(
|
|
227
|
-
if (i === 0) return `${indent}Subtotal`;
|
|
228
|
-
const aggVal = entry.aggregatedValues![col.field];
|
|
229
|
-
if (aggVal === undefined || aggVal === null) return '';
|
|
230
|
-
let formatted: unknown = aggVal;
|
|
231
|
-
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
232
|
-
else if (aggMod[col.field]) formatted = formatAggregationValue(aggVal, aggMod[col.field] as string);
|
|
233
|
-
return String(formatted);
|
|
234
|
-
}));
|
|
292
|
+
const texts = aggregateTexts(entry.aggregatedValues);
|
|
293
|
+
body.push(texts.map((text, i) => (i === 0 ? `${indent}${summaryLabelText('Subtotal', text)}` : text)));
|
|
235
294
|
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const aggVal = entry.aggregatedValues![col.field];
|
|
239
|
-
if (aggVal === undefined || aggVal === null) return '';
|
|
240
|
-
let formatted: unknown = aggVal;
|
|
241
|
-
if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
242
|
-
else if (aggMod[col.field]) formatted = formatAggregationValue(aggVal, aggMod[col.field] as string);
|
|
243
|
-
return String(formatted);
|
|
244
|
-
});
|
|
245
|
-
foot = [footRow];
|
|
295
|
+
const texts = aggregateTexts(entry.aggregatedValues);
|
|
296
|
+
foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text))];
|
|
246
297
|
}
|
|
247
298
|
});
|
|
248
299
|
} else {
|
|
249
300
|
body = rowsToExport.map(row => exportColumns.map(col => resolveValue(row, col)));
|
|
250
301
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (aggVal === undefined || aggVal === null) return '';
|
|
259
|
-
let formatted: unknown = aggVal;
|
|
260
|
-
if (col.valueFormatter) {
|
|
261
|
-
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
262
|
-
} else if (aggregationModel[col.field]) {
|
|
263
|
-
formatted = formatAggregationValue(aggVal, aggregationModel[col.field] as string);
|
|
264
|
-
}
|
|
265
|
-
return String(formatted);
|
|
266
|
-
});
|
|
267
|
-
foot = [footRow];
|
|
302
|
+
const totals = aggregationModel
|
|
303
|
+
? aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, aggregationResult, aggregationModel)
|
|
304
|
+
: null;
|
|
305
|
+
if (totals && aggregationModel) {
|
|
306
|
+
const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
|
|
307
|
+
const texts = aggregateTexts(totals);
|
|
308
|
+
foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text))];
|
|
268
309
|
}
|
|
269
310
|
}
|
|
270
311
|
|
|
312
|
+
body = body.map(cells => cells.map(pdfText));
|
|
313
|
+
if (foot) foot = foot.map(cells => cells.map(pdfText));
|
|
314
|
+
|
|
315
|
+
if (replacedCharacters) {
|
|
316
|
+
console.warn(
|
|
317
|
+
'[exportToPdf] Some characters cannot be drawn with the built-in Helvetica font and were replaced with "?". ' +
|
|
318
|
+
'Pass the `font` option (a Unicode .ttf, e.g. Noto Sans) to draw them.'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
271
322
|
// --- Column widths ---
|
|
272
323
|
const columnStyles = computeColumnStyles(exportColumns, usableWidth);
|
|
273
324
|
|
|
274
|
-
const [hr, hg, hb] = hexToRgb(headerBackgroundColor);
|
|
275
|
-
const [tr, tg, tb] = hexToRgb(headerTextColor);
|
|
325
|
+
const [hr, hg, hb] = hexToRgb(headerBackgroundColor, DEFAULT_HEADER_BACKGROUND);
|
|
326
|
+
const [tr, tg, tb] = hexToRgb(headerTextColor, DEFAULT_HEADER_TEXT);
|
|
276
327
|
|
|
277
328
|
autoTable(doc, {
|
|
278
329
|
head,
|
|
279
330
|
body,
|
|
280
331
|
...(foot ? { foot } : {}),
|
|
281
332
|
startY,
|
|
282
|
-
styles: { fontSize, cellPadding: 3, font:
|
|
333
|
+
styles: { fontSize, cellPadding: 3, font: fontName, overflow: 'linebreak' },
|
|
283
334
|
headStyles: {
|
|
284
335
|
fillColor: [hr, hg, hb],
|
|
285
336
|
textColor: [tr, tg, tb],
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GridColDef, GridGroupedExportRow, GridRowModel } from '../../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The label for a grouped-export group-header row, matching what the grid shows:
|
|
5
|
+
* the entry's own groupLabel (set by getGroupedExportRows), else the grouping column's
|
|
6
|
+
* groupingValueFormatter, else the documented "field: value" default.
|
|
7
|
+
*/
|
|
8
|
+
export function groupHeaderLabel<R extends GridRowModel>(entry: GridGroupedExportRow, columns: GridColDef<R>[]): string {
|
|
9
|
+
if (entry.groupLabel !== undefined) return entry.groupLabel;
|
|
10
|
+
const field = entry.groupField ?? '';
|
|
11
|
+
const formatter = columns.find(col => col.field === field)?.groupingValueFormatter;
|
|
12
|
+
if (formatter) return formatter({ field, value: entry.groupValue });
|
|
13
|
+
return `${field}: ${String(entry.groupValue ?? '')}`;
|
|
14
|
+
}
|