@opencorestack/opengridx 1.0.7 → 1.2.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 +30 -0
- package/dist/index.d.ts +100 -12
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +3199 -3015
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +85 -0
- package/docs/components/cell.md +14 -0
- package/docs/components/filter-panel.md +13 -0
- package/docs/components/pagination.md +26 -0
- package/docs/components/row.md +26 -0
- package/docs/features/pdf-export.md +110 -0
- package/docs/roadmap.md +8 -0
- package/lib/components/Cell/Cell.tsx +44 -44
- package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
- package/lib/components/DataGrid/DataGrid.css +11 -0
- package/lib/components/DataGrid/DataGrid.tsx +30 -6
- package/lib/components/DataGrid/GridListView.tsx +4 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
- package/lib/components/Pagination/Pagination.test.tsx +64 -0
- package/lib/components/Pagination/Pagination.tsx +15 -4
- package/lib/components/Row/Row.tsx +6 -3
- package/lib/hooks/core/useGridColumns.tsx +11 -11
- package/lib/hooks/core/useGridControlledState.test.ts +86 -0
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
- package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
- package/lib/hooks/useRowGrouping.test.ts +84 -0
- package/lib/hooks/useRowGrouping.ts +38 -13
- package/lib/hooks/useTreeData.ts +22 -2
- package/lib/index.ts +5 -1
- package/lib/types/index.ts +107 -12
- package/lib/utils/export/exportToPdf.test.ts +121 -0
- package/lib/utils/export/exportToPdf.ts +277 -0
- package/lib/utils/export/index.ts +3 -0
- package/lib/utils/export/pdf-types.d.ts +19 -0
- package/package.json +9 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GridColDef,
|
|
3
|
+
GridRowModel,
|
|
4
|
+
GridFilterModel,
|
|
5
|
+
PdfExportOptions,
|
|
6
|
+
} from '../../types';
|
|
7
|
+
import { formatAggregationValue } from '../../hooks/features/useAggregation';
|
|
8
|
+
|
|
9
|
+
// jsPDF plugin augments the jsPDF prototype — we type only what we use
|
|
10
|
+
interface JsPDFDoc {
|
|
11
|
+
autoTable: (opts: AutoTableOptions) => void;
|
|
12
|
+
save: (filename: string) => void;
|
|
13
|
+
setFontSize: (size: number) => void;
|
|
14
|
+
setFont: (name: string, style: string) => void;
|
|
15
|
+
setTextColor: (r: number, g: number, b: number) => void;
|
|
16
|
+
text: (text: string, x: number, y: number, opts?: { align?: string }) => void;
|
|
17
|
+
addImage: (data: string, format: string, x: number, y: number, w: number, h: number) => void;
|
|
18
|
+
line: (x1: number, y1: number, x2: number, y2: number) => void;
|
|
19
|
+
setDrawColor: (r: number, g: number, b: number) => void;
|
|
20
|
+
setLineWidth: (w: number) => void;
|
|
21
|
+
setPage: (page: number) => void;
|
|
22
|
+
lastAutoTable: { finalY: number };
|
|
23
|
+
internal: {
|
|
24
|
+
pageSize: { getWidth: () => number; getHeight: () => number };
|
|
25
|
+
getNumberOfPages: () => number;
|
|
26
|
+
};
|
|
27
|
+
getNumberOfPages: () => number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AutoTableOptions {
|
|
31
|
+
head: string[][];
|
|
32
|
+
body: string[][];
|
|
33
|
+
foot?: string[][];
|
|
34
|
+
startY?: number;
|
|
35
|
+
styles?: Record<string, unknown>;
|
|
36
|
+
headStyles?: Record<string, unknown>;
|
|
37
|
+
alternateRowStyles?: Record<string, unknown>;
|
|
38
|
+
footStyles?: Record<string, unknown>;
|
|
39
|
+
columnStyles?: Record<number, { cellWidth: number }>;
|
|
40
|
+
showFoot?: string;
|
|
41
|
+
showHead?: string;
|
|
42
|
+
margin?: { top: number; left: number; right: number; bottom: number };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
46
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
47
|
+
if (!result) return [79, 70, 229];
|
|
48
|
+
return [
|
|
49
|
+
parseInt(result[1], 16),
|
|
50
|
+
parseInt(result[2], 16),
|
|
51
|
+
parseInt(result[3], 16),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getExportColumns<R extends GridRowModel>(columns: GridColDef<R>[]): GridColDef<R>[] {
|
|
56
|
+
return columns.filter(col => {
|
|
57
|
+
if (col.exportable === false) return false;
|
|
58
|
+
if (col.field === '__check__' || col.field === '__actions__') return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveValue<R extends GridRowModel>(row: R, col: GridColDef<R>): string {
|
|
64
|
+
let value: unknown = (row as GridRowModel)[col.field];
|
|
65
|
+
if (col.valueGetter) {
|
|
66
|
+
value = col.valueGetter({ row, field: col.field, value });
|
|
67
|
+
}
|
|
68
|
+
if (col.valueFormatter && value !== undefined && value !== null) {
|
|
69
|
+
value = col.valueFormatter({ value, row, field: col.field });
|
|
70
|
+
}
|
|
71
|
+
return String(value ?? '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function colWeight<R extends GridRowModel>(col: GridColDef<R>): number {
|
|
75
|
+
const w = typeof col.width === 'number' ? col.width : undefined;
|
|
76
|
+
return w ?? (col.flex ?? 1) * 100;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function computeColumnStyles<R extends GridRowModel>(
|
|
80
|
+
columns: GridColDef<R>[],
|
|
81
|
+
usableWidth: number
|
|
82
|
+
): Record<number, { cellWidth: number }> {
|
|
83
|
+
const totalWeight = columns.reduce((sum, col) => sum + colWeight(col), 0);
|
|
84
|
+
const styles: Record<number, { cellWidth: number }> = {};
|
|
85
|
+
columns.forEach((col, i) => {
|
|
86
|
+
styles[i] = { cellWidth: (colWeight(col) / totalWeight) * usableWidth };
|
|
87
|
+
});
|
|
88
|
+
return styles;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatFilterSummary<R extends GridRowModel>(
|
|
92
|
+
filterModel: GridFilterModel,
|
|
93
|
+
columns: GridColDef<R>[]
|
|
94
|
+
): string {
|
|
95
|
+
if (!filterModel.items || filterModel.items.length === 0) return '';
|
|
96
|
+
return filterModel.items
|
|
97
|
+
.flatMap(item => {
|
|
98
|
+
// Only handle leaf GridFilterItem — skip GridFilterGroup (which has 'items')
|
|
99
|
+
if ('items' in item) return [];
|
|
100
|
+
const col = columns.find(c => c.field === item.field);
|
|
101
|
+
const label = col?.headerName ?? item.field;
|
|
102
|
+
const val = item.value !== undefined && item.value !== null ? String(item.value) : '';
|
|
103
|
+
return [`${label} ${item.operator} "${val}"`];
|
|
104
|
+
})
|
|
105
|
+
.join(' • ');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function exportToPdf<R extends GridRowModel>(
|
|
109
|
+
rows: R[],
|
|
110
|
+
columns: GridColDef<R>[],
|
|
111
|
+
options: PdfExportOptions = {}
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
// Lazy-load peer deps — provides a clear error if not installed
|
|
114
|
+
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
115
|
+
try {
|
|
116
|
+
const mod = await import('jspdf');
|
|
117
|
+
JsPDF = mod.default as new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
118
|
+
await import('jspdf-autotable');
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"exportToPdf requires 'jspdf' and 'jspdf-autotable'. Run: npm install jspdf jspdf-autotable"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
fileName = 'export',
|
|
127
|
+
title,
|
|
128
|
+
logoUrl,
|
|
129
|
+
orientation = 'landscape',
|
|
130
|
+
selectedRows,
|
|
131
|
+
aggregationResult,
|
|
132
|
+
aggregationModel,
|
|
133
|
+
filterModel,
|
|
134
|
+
alternateRowColor = true,
|
|
135
|
+
headerBackgroundColor = '#4f46e5',
|
|
136
|
+
headerTextColor = '#ffffff',
|
|
137
|
+
fontSize = 9,
|
|
138
|
+
} = options;
|
|
139
|
+
|
|
140
|
+
const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
|
|
141
|
+
const pageWidth = doc.internal.pageSize.getWidth();
|
|
142
|
+
const pageHeight = doc.internal.pageSize.getHeight();
|
|
143
|
+
const MARGIN = 14;
|
|
144
|
+
const usableWidth = pageWidth - MARGIN * 2;
|
|
145
|
+
|
|
146
|
+
const exportColumns = getExportColumns(columns);
|
|
147
|
+
const rowsToExport = selectedRows && selectedRows.length > 0
|
|
148
|
+
? rows.filter(r => selectedRows.includes(r.id as string | number))
|
|
149
|
+
: rows;
|
|
150
|
+
|
|
151
|
+
// --- Header block ---
|
|
152
|
+
let startY = MARGIN;
|
|
153
|
+
|
|
154
|
+
if (title) {
|
|
155
|
+
let cursorX = MARGIN;
|
|
156
|
+
let cursorY = MARGIN + 8;
|
|
157
|
+
|
|
158
|
+
// Logo
|
|
159
|
+
if (logoUrl) {
|
|
160
|
+
try {
|
|
161
|
+
doc.addImage(logoUrl, 'PNG', cursorX, MARGIN, 10, 10);
|
|
162
|
+
cursorX += 14;
|
|
163
|
+
} catch {
|
|
164
|
+
// logo failed to load — skip silently
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Title
|
|
169
|
+
doc.setFontSize(16);
|
|
170
|
+
doc.setFont('helvetica', 'bold');
|
|
171
|
+
doc.setTextColor(30, 41, 59); // #1e293b
|
|
172
|
+
doc.text(title, cursorX, cursorY);
|
|
173
|
+
cursorY += 6;
|
|
174
|
+
|
|
175
|
+
// Subtitle: date + row count
|
|
176
|
+
doc.setFontSize(8);
|
|
177
|
+
doc.setFont('helvetica', 'normal');
|
|
178
|
+
doc.setTextColor(100, 116, 139); // #64748b
|
|
179
|
+
const exportedAt = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
180
|
+
doc.text(
|
|
181
|
+
`Exported: ${exportedAt} • ${rowsToExport.length} row${rowsToExport.length !== 1 ? 's' : ''}`,
|
|
182
|
+
cursorX,
|
|
183
|
+
cursorY
|
|
184
|
+
);
|
|
185
|
+
cursorY += 5;
|
|
186
|
+
|
|
187
|
+
// Filter summary
|
|
188
|
+
if (filterModel) {
|
|
189
|
+
const summary = formatFilterSummary(filterModel, columns);
|
|
190
|
+
if (summary) {
|
|
191
|
+
doc.text(`Filters: ${summary}`, cursorX, cursorY);
|
|
192
|
+
cursorY += 5;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Separator line
|
|
197
|
+
doc.setDrawColor(226, 232, 240); // #e2e8f0
|
|
198
|
+
doc.setLineWidth(0.3);
|
|
199
|
+
doc.line(MARGIN, cursorY + 2, pageWidth - MARGIN, cursorY + 2);
|
|
200
|
+
startY = cursorY + 6;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Build table data ---
|
|
204
|
+
const head = [exportColumns.map(col => col.headerName ?? col.field)];
|
|
205
|
+
|
|
206
|
+
const body = rowsToExport.map(row =>
|
|
207
|
+
exportColumns.map(col => resolveValue(row, col))
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// --- Aggregation footer ---
|
|
211
|
+
let foot: string[][] | undefined;
|
|
212
|
+
if (aggregationResult && aggregationModel) {
|
|
213
|
+
const footRow = exportColumns.map((col, i) => {
|
|
214
|
+
if (i === 0) {
|
|
215
|
+
// First column shows TOTAL label if any aggregation exists
|
|
216
|
+
const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
|
|
217
|
+
return hasAgg ? 'TOTAL' : '';
|
|
218
|
+
}
|
|
219
|
+
const aggVal = aggregationResult[col.field];
|
|
220
|
+
if (aggVal === undefined || aggVal === null) return '';
|
|
221
|
+
let formatted: unknown = aggVal;
|
|
222
|
+
if (col.valueFormatter) {
|
|
223
|
+
formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
|
|
224
|
+
} else if (aggregationModel[col.field]) {
|
|
225
|
+
formatted = formatAggregationValue(aggVal, aggregationModel[col.field] as string);
|
|
226
|
+
}
|
|
227
|
+
return String(formatted);
|
|
228
|
+
});
|
|
229
|
+
foot = [footRow];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Column widths ---
|
|
233
|
+
const columnStyles = computeColumnStyles(exportColumns, usableWidth);
|
|
234
|
+
|
|
235
|
+
const [hr, hg, hb] = hexToRgb(headerBackgroundColor);
|
|
236
|
+
const [tr, tg, tb] = hexToRgb(headerTextColor);
|
|
237
|
+
|
|
238
|
+
doc.autoTable({
|
|
239
|
+
head,
|
|
240
|
+
body,
|
|
241
|
+
...(foot ? { foot } : {}),
|
|
242
|
+
startY,
|
|
243
|
+
styles: { fontSize, cellPadding: 3, font: 'helvetica', overflow: 'linebreak' },
|
|
244
|
+
headStyles: {
|
|
245
|
+
fillColor: [hr, hg, hb],
|
|
246
|
+
textColor: [tr, tg, tb],
|
|
247
|
+
fontStyle: 'bold',
|
|
248
|
+
},
|
|
249
|
+
...(alternateRowColor
|
|
250
|
+
? { alternateRowStyles: { fillColor: [248, 250, 252] } }
|
|
251
|
+
: {}),
|
|
252
|
+
...(foot
|
|
253
|
+
? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold' } }
|
|
254
|
+
: {}),
|
|
255
|
+
columnStyles,
|
|
256
|
+
showFoot: foot ? 'lastPage' : 'never',
|
|
257
|
+
showHead: 'everyPage',
|
|
258
|
+
margin: { top: MARGIN, left: MARGIN, right: MARGIN, bottom: MARGIN + 6 },
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Two-pass page numbering — stamp all pages after the table is fully rendered
|
|
262
|
+
// so each stamp reads the correct final total (not the running count during draw)
|
|
263
|
+
const totalPages = doc.getNumberOfPages();
|
|
264
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
265
|
+
doc.setPage(i);
|
|
266
|
+
doc.setFontSize(8);
|
|
267
|
+
doc.setTextColor(150, 150, 150);
|
|
268
|
+
doc.text(
|
|
269
|
+
`Page ${i} of ${totalPages}`,
|
|
270
|
+
pageWidth - MARGIN,
|
|
271
|
+
pageHeight - 8,
|
|
272
|
+
{ align: 'right' }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
doc.save(`${fileName}.pdf`);
|
|
277
|
+
}
|
|
@@ -553,3 +553,6 @@ function downloadFile(content: string, fileName: string, mimeType: string): void
|
|
|
553
553
|
document.body.removeChild(link);
|
|
554
554
|
URL.revokeObjectURL(url);
|
|
555
555
|
}
|
|
556
|
+
|
|
557
|
+
export { exportToPdf } from './exportToPdf';
|
|
558
|
+
export type { PdfExportOptions } from '../../types';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ambient declarations for optional peer dependencies.
|
|
2
|
+
// These packages are listed in peerDependenciesMeta as optional and are NOT
|
|
3
|
+
// installed in devDependencies. The declarations here let TypeScript accept
|
|
4
|
+
// the dynamic imports without requiring the packages at build time.
|
|
5
|
+
// This file is excluded from the shipped .d.ts rollup so it does not
|
|
6
|
+
// conflict with a consumer's real jspdf types.
|
|
7
|
+
|
|
8
|
+
declare module 'jspdf' {
|
|
9
|
+
// Minimal shape — exportToPdf.ts only uses the JsPDFDoc interface it defines.
|
|
10
|
+
// The full jsPDF types are available when the consumer installs the package.
|
|
11
|
+
const JsPDF: new (opts?: Record<string, unknown>) => unknown;
|
|
12
|
+
export default JsPDF;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module 'jspdf-autotable' {
|
|
16
|
+
// Side-effect import that augments jsPDF.prototype.autoTable.
|
|
17
|
+
const autoTable: unknown;
|
|
18
|
+
export default autoTable;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|
|
@@ -78,12 +78,20 @@
|
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"exceljs": ">=4.0.0",
|
|
81
|
+
"jspdf": ">=2.5.0",
|
|
82
|
+
"jspdf-autotable": ">=3.8.0",
|
|
81
83
|
"react": ">=18.0.0",
|
|
82
84
|
"react-dom": ">=18.0.0"
|
|
83
85
|
},
|
|
84
86
|
"peerDependenciesMeta": {
|
|
85
87
|
"exceljs": {
|
|
86
88
|
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"jspdf": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"jspdf-autotable": {
|
|
94
|
+
"optional": true
|
|
87
95
|
}
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|