@opencorestack/opengridx 3.0.1 → 3.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 +24 -0
- package/README.md +4 -2
- package/dist/index.d.ts +61 -0
- package/dist/opengridx.es.js +4279 -4132
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +14 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +18 -1
- package/docs/architecture/datagrid-orchestration.md +1 -0
- package/docs/components/column-resize.md +13 -1
- package/docs/features/pdf-export.md +38 -2
- package/docs/features/sorting-pagination.md +26 -0
- package/docs/migration/v2-to-v3.md +1 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +12 -3
- package/lib/components/DataGrid/DataGrid.tsx +3 -0
- package/lib/components/Header/Header.tsx +4 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +2 -0
- package/lib/hooks/core/useGridColumnAutosize.ts +52 -0
- package/lib/hooks/core/useGridRowPipeline.ts +2 -2
- package/lib/hooks/useRowGrouping.ts +20 -3
- package/lib/hooks/useTreeData.ts +8 -1
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +63 -0
- package/lib/utils/columnAutosize.ts +56 -0
- package/lib/utils/export/exportToPdf.ts +167 -21
- package/lib/utils/export/index.ts +1 -1
- package/lib/utils/pivot/index.ts +11 -2
- package/lib/utils/sorting/index.ts +77 -24
- package/llms.txt +10 -2
- package/package.json +1 -1
|
@@ -57,8 +57,49 @@ interface AutoTableOptions {
|
|
|
57
57
|
showFoot?: string;
|
|
58
58
|
showHead?: string;
|
|
59
59
|
margin?: { top: number; left: number; right: number; bottom: number };
|
|
60
|
+
willDrawPage?: (data: AutoTableHookData) => void;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** The part of jspdf-autotable's hook data used here; `settings` and `cursor` are the table's live objects. */
|
|
64
|
+
interface AutoTableHookData {
|
|
65
|
+
settings: { showHead: string };
|
|
66
|
+
cursor: { y: number } | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type AutoTableFn = (doc: JsPDFDoc, opts: AutoTableOptions) => void;
|
|
70
|
+
|
|
71
|
+
/** How the work is sliced. Output does not depend on these values; tests shrink them. */
|
|
72
|
+
export interface PdfExportTuning {
|
|
73
|
+
/** Longest stretch of synchronous work before yielding to the event loop, in ms. */
|
|
74
|
+
sliceMs: number;
|
|
75
|
+
/** Table rows drawn by the first autoTable call; later calls adapt to `sliceMs`. Rounded to even. */
|
|
76
|
+
renderChunkRows: number;
|
|
77
|
+
/** Resize later chunks to fit `sliceMs` (default behaviour); false keeps every chunk at `renderChunkRows`. */
|
|
78
|
+
adaptive: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DEFAULT_TUNING: PdfExportTuning = { sliceMs: 30, renderChunkRows: 40, adaptive: true };
|
|
82
|
+
|
|
83
|
+
/** Default `maxRows`: above this a PDF is tens of MB and takes seconds to build and open. */
|
|
84
|
+
export const PDF_MAX_ROWS_DEFAULT = 20_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Let the browser paint and run other tasks. A macrotask on purpose: `scheduler.yield()` resumes
|
|
88
|
+
* ahead of queued tasks, which starves timers and React's scheduler (a progress bar would not
|
|
89
|
+
* update until the export finished).
|
|
90
|
+
*/
|
|
91
|
+
function yieldToEventLoop(): Promise<void> {
|
|
92
|
+
return new Promise(resolve => setTimeout(resolve, 0));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function abortError(): DOMException {
|
|
96
|
+
return new DOMException('PDF export was aborted', 'AbortError');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = (): number => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
100
|
+
|
|
101
|
+
const evenAtLeast2 = (n: number): number => Math.max(2, Math.floor(n / 2) * 2);
|
|
102
|
+
|
|
62
103
|
type Rgb = [number, number, number];
|
|
63
104
|
|
|
64
105
|
const DEFAULT_HEADER_BACKGROUND: Rgb = [79, 70, 229];
|
|
@@ -144,21 +185,37 @@ const LINE_HEIGHT_MM_PER_PT = 1.15 * 0.3528;
|
|
|
144
185
|
|
|
145
186
|
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: PdfExportOptions<R>): Promise<void>;
|
|
146
187
|
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: PdfExportOptions<R>): Promise<void>;
|
|
147
|
-
export
|
|
188
|
+
export function exportToPdf<R extends GridRowModel>(
|
|
148
189
|
rows: R[],
|
|
149
190
|
columns: GridColDef<R>[],
|
|
150
191
|
options: PdfExportOptions<R> = {}
|
|
151
192
|
): Promise<void> {
|
|
193
|
+
return runPdfExport(rows, columns, options, DEFAULT_TUNING);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** `exportToPdf` with explicit slicing. Internal: not part of the public API. */
|
|
197
|
+
export async function runPdfExport<R extends GridRowModel>(
|
|
198
|
+
rows: R[],
|
|
199
|
+
columns: GridColDef<R>[],
|
|
200
|
+
options: PdfExportOptions<R>,
|
|
201
|
+
tuning: PdfExportTuning,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const { signal, onProgress } = options;
|
|
204
|
+
const checkAbort = (): void => {
|
|
205
|
+
if (signal?.aborted) throw abortError();
|
|
206
|
+
};
|
|
207
|
+
checkAbort();
|
|
208
|
+
|
|
152
209
|
// Lazy-load peer deps — provides a clear error if not installed
|
|
153
210
|
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
154
|
-
let autoTable:
|
|
211
|
+
let autoTable: AutoTableFn;
|
|
155
212
|
try {
|
|
156
213
|
const [jspdfMod, autoTableMod] = await Promise.all([
|
|
157
214
|
import('jspdf'),
|
|
158
215
|
import('jspdf-autotable'),
|
|
159
216
|
]);
|
|
160
217
|
JsPDF = jspdfMod.default as unknown as new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
161
|
-
autoTable = autoTableMod.default as unknown as
|
|
218
|
+
autoTable = autoTableMod.default as unknown as AutoTableFn;
|
|
162
219
|
} catch {
|
|
163
220
|
throw new Error(
|
|
164
221
|
"exportToPdf requires 'jspdf' and 'jspdf-autotable'. Run: npm install jspdf jspdf-autotable"
|
|
@@ -181,7 +238,21 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
181
238
|
fontSize = 9,
|
|
182
239
|
groupedRows,
|
|
183
240
|
font,
|
|
241
|
+
maxRows = PDF_MAX_ROWS_DEFAULT,
|
|
184
242
|
} = options;
|
|
243
|
+
checkAbort();
|
|
244
|
+
|
|
245
|
+
// Time-sliced work: `maybeYield` yields once the current slice has run for `sliceMs`.
|
|
246
|
+
let sliceStart = now();
|
|
247
|
+
const maybeYield = async (): Promise<void> => {
|
|
248
|
+
if (now() - sliceStart < tuning.sliceMs) return;
|
|
249
|
+
await yieldToEventLoop();
|
|
250
|
+
checkAbort();
|
|
251
|
+
sliceStart = now();
|
|
252
|
+
};
|
|
253
|
+
const report = (phase: 'prepare' | 'render' | 'save', done: number, total: number): void => {
|
|
254
|
+
onProgress?.({ phase, done, total });
|
|
255
|
+
};
|
|
185
256
|
|
|
186
257
|
const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
|
|
187
258
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
@@ -274,30 +345,48 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
274
345
|
const aggregateTexts = (values: Record<string, unknown>): string[] =>
|
|
275
346
|
exportColumns.map(col => formatExportAggregate(col, aggMod[col.field], values[col.field], values));
|
|
276
347
|
|
|
277
|
-
|
|
348
|
+
const body: string[][] = [];
|
|
278
349
|
let foot: string[][] | undefined;
|
|
350
|
+
const pushRow = (cells: string[]): void => {
|
|
351
|
+
body.push(cells.map(pdfText));
|
|
352
|
+
};
|
|
279
353
|
|
|
280
354
|
if (useGrouped) {
|
|
281
355
|
// Grouped export: flatten the ordered list into body rows with indentation
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
const total = groupedRows.length;
|
|
357
|
+
report('prepare', 0, total);
|
|
358
|
+
for (let index = 0; index < total; index++) {
|
|
359
|
+
const entry: GridGroupedExportRow = groupedRows[index];
|
|
284
360
|
const indent = ' '.repeat(entry.depth * 2);
|
|
285
361
|
if (entry.type === 'group-header') {
|
|
286
362
|
const label = `${indent}${groupHeaderLabel(entry, columns)}`;
|
|
287
|
-
|
|
363
|
+
pushRow(exportColumns.map((_, i) => i === 0 ? label : ''));
|
|
288
364
|
} else if (entry.type === 'leaf' && entry.row) {
|
|
289
365
|
const row = entry.row as R;
|
|
290
|
-
|
|
366
|
+
pushRow(exportColumns.map((col, i) => (i === 0 ? indent : '') + resolveValue(row, col)));
|
|
291
367
|
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
|
|
292
368
|
const texts = aggregateTexts(entry.aggregatedValues);
|
|
293
|
-
|
|
369
|
+
pushRow(texts.map((text, i) => (i === 0 ? `${indent}${summaryLabelText('Subtotal', text)}` : text)));
|
|
294
370
|
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
295
371
|
const texts = aggregateTexts(entry.aggregatedValues);
|
|
296
|
-
foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text))];
|
|
372
|
+
foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text)).map(pdfText)];
|
|
297
373
|
}
|
|
298
|
-
|
|
374
|
+
if ((index & 63) === 63) {
|
|
375
|
+
report('prepare', index + 1, total);
|
|
376
|
+
await maybeYield();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
report('prepare', total, total);
|
|
299
380
|
} else {
|
|
300
|
-
|
|
381
|
+
const total = rowsToExport.length;
|
|
382
|
+
report('prepare', 0, total);
|
|
383
|
+
for (let index = 0; index < total; index++) {
|
|
384
|
+
pushRow(exportColumns.map(col => resolveValue(rowsToExport[index], col)));
|
|
385
|
+
if ((index & 63) === 63) {
|
|
386
|
+
report('prepare', index + 1, total);
|
|
387
|
+
await maybeYield();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
301
390
|
|
|
302
391
|
const totals = aggregationModel
|
|
303
392
|
? aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, aggregationResult, aggregationModel)
|
|
@@ -305,12 +394,19 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
305
394
|
if (totals && aggregationModel) {
|
|
306
395
|
const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
|
|
307
396
|
const texts = aggregateTexts(totals);
|
|
308
|
-
foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text))];
|
|
397
|
+
foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text)).map(pdfText)];
|
|
309
398
|
}
|
|
399
|
+
report('prepare', total, total);
|
|
310
400
|
}
|
|
401
|
+
checkAbort();
|
|
311
402
|
|
|
312
|
-
body
|
|
313
|
-
|
|
403
|
+
if (body.length > maxRows && process.env.NODE_ENV !== 'production') {
|
|
404
|
+
console.warn(
|
|
405
|
+
`[exportToPdf] Exporting ${body.length} table rows (maxRows: ${maxRows}). PDFs this large are slow to build ` +
|
|
406
|
+
'and open and can reach tens of MB. Prefer exportToCsv or exportToExcelAdvanced for full-dataset exports, ' +
|
|
407
|
+
'or pass a larger `maxRows` to silence this warning.'
|
|
408
|
+
);
|
|
409
|
+
}
|
|
314
410
|
|
|
315
411
|
if (replacedCharacters) {
|
|
316
412
|
console.warn(
|
|
@@ -325,11 +421,8 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
325
421
|
const [hr, hg, hb] = hexToRgb(headerBackgroundColor, DEFAULT_HEADER_BACKGROUND);
|
|
326
422
|
const [tr, tg, tb] = hexToRgb(headerTextColor, DEFAULT_HEADER_TEXT);
|
|
327
423
|
|
|
328
|
-
|
|
424
|
+
const baseTableOptions: Omit<AutoTableOptions, 'body' | 'startY'> = {
|
|
329
425
|
head,
|
|
330
|
-
body,
|
|
331
|
-
...(foot ? { foot } : {}),
|
|
332
|
-
startY,
|
|
333
426
|
styles: { fontSize, cellPadding: 3, font: fontName, overflow: 'linebreak' },
|
|
334
427
|
headStyles: {
|
|
335
428
|
fillColor: [hr, hg, hb],
|
|
@@ -343,10 +436,59 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
343
436
|
? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold', textColor: [0, 0, 0] } }
|
|
344
437
|
: {}),
|
|
345
438
|
columnStyles,
|
|
346
|
-
showFoot: foot ? 'lastPage' : 'never',
|
|
347
439
|
showHead: 'everyPage',
|
|
348
440
|
margin: { top: MARGIN, left: MARGIN, right: MARGIN, bottom: MARGIN + 6 },
|
|
349
|
-
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// The body is drawn as a series of autoTable calls, each continuing where the previous one
|
|
444
|
+
// ended, so the drawing phase yields too. Column widths are fixed (columnStyles), chunks hold
|
|
445
|
+
// an even number of rows (alternate shading keeps its parity), only the last chunk carries the
|
|
446
|
+
// footer, and a continuation chunk skips the header on the page it starts on but repeats it on
|
|
447
|
+
// every page it adds: the same pages a single call would produce.
|
|
448
|
+
const totalRows = body.length;
|
|
449
|
+
report('render', 0, totalRows);
|
|
450
|
+
let drawn = 0;
|
|
451
|
+
let chunkRows = evenAtLeast2(tuning.renderChunkRows);
|
|
452
|
+
do {
|
|
453
|
+
const chunk = body.slice(drawn, drawn + chunkRows);
|
|
454
|
+
const isFirst = drawn === 0;
|
|
455
|
+
const isLast = drawn + chunk.length >= totalRows;
|
|
456
|
+
const continueY = isFirst ? startY : doc.lastAutoTable.finalY;
|
|
457
|
+
let firstPageOfChunk = true;
|
|
458
|
+
const started = now();
|
|
459
|
+
autoTable(doc, {
|
|
460
|
+
...baseTableOptions,
|
|
461
|
+
body: chunk,
|
|
462
|
+
...(isLast && foot ? { foot } : {}),
|
|
463
|
+
// A continuation starts at the top margin so autoTable's "does the table fit below
|
|
464
|
+
// startY" check never adds a page; the hook then moves the cursor to where the previous
|
|
465
|
+
// chunk ended, and each row breaks the page exactly as it would in a single table.
|
|
466
|
+
startY: isFirst ? startY : MARGIN,
|
|
467
|
+
showFoot: isLast && foot ? 'lastPage' : 'never',
|
|
468
|
+
...(isFirst ? {} : {
|
|
469
|
+
willDrawPage: (data: AutoTableHookData) => {
|
|
470
|
+
if (firstPageOfChunk) {
|
|
471
|
+
firstPageOfChunk = false;
|
|
472
|
+
// Same page as the previous chunk: its header is already drawn.
|
|
473
|
+
data.settings.showHead = 'never';
|
|
474
|
+
if (data.cursor) data.cursor.y = continueY;
|
|
475
|
+
} else {
|
|
476
|
+
data.settings.showHead = 'everyPage';
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
});
|
|
481
|
+
drawn += chunk.length;
|
|
482
|
+
report('render', drawn, totalRows);
|
|
483
|
+
const elapsed = now() - started;
|
|
484
|
+
// Aim each call at one slice; grow at most 4x at a time.
|
|
485
|
+
if (tuning.adaptive) chunkRows = evenAtLeast2(elapsed > 0 ? Math.min(chunkRows * 4, chunkRows * (tuning.sliceMs / elapsed)) : chunkRows * 4);
|
|
486
|
+
if (!isLast) {
|
|
487
|
+
await yieldToEventLoop();
|
|
488
|
+
checkAbort();
|
|
489
|
+
sliceStart = now();
|
|
490
|
+
}
|
|
491
|
+
} while (drawn < totalRows);
|
|
350
492
|
|
|
351
493
|
// Two-pass page numbering — stamp all pages after the table is fully rendered
|
|
352
494
|
// so each stamp reads the correct final total (not the running count during draw)
|
|
@@ -361,7 +503,11 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
361
503
|
pageHeight - 8,
|
|
362
504
|
{ align: 'right' }
|
|
363
505
|
);
|
|
506
|
+
await maybeYield();
|
|
364
507
|
}
|
|
365
508
|
|
|
509
|
+
checkAbort();
|
|
510
|
+
report('save', 0, 1);
|
|
366
511
|
doc.save(`${fileName}.pdf`);
|
|
512
|
+
report('save', 1, 1);
|
|
367
513
|
}
|
|
@@ -690,4 +690,4 @@ function downloadFile(content: string, fileName: string, mimeType: string): void
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
export { exportToPdf } from './exportToPdf';
|
|
693
|
-
export type { PdfExportOptions } from '../../types';
|
|
693
|
+
export type { PdfExportOptions, PdfExportProgress } from '../../types';
|
package/lib/utils/pivot/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
isEmptyAggregateValue,
|
|
19
19
|
} from '../aggregation';
|
|
20
20
|
import { createRowFilter } from '../filtering';
|
|
21
|
-
import { compareValues } from '../sorting';
|
|
21
|
+
import { applySortDirection, callSortComparator, compareValues } from '../sorting';
|
|
22
22
|
import { buildColumnLookup } from '../columnLookup';
|
|
23
23
|
import { getCellValue, getFormattedValue } from '../values';
|
|
24
24
|
|
|
@@ -256,7 +256,16 @@ export function computePivot(
|
|
|
256
256
|
if (sortModel && sortModel.length > 0) {
|
|
257
257
|
kept = [...kept].sort((a, b) => {
|
|
258
258
|
for (const item of sortModel) {
|
|
259
|
-
const
|
|
259
|
+
const va = sortValue(a.row, item.field);
|
|
260
|
+
const vb = sortValue(b.row, item.field);
|
|
261
|
+
const comparator = isRowField(item.field) ? colDefMap.get(item.field)?.sortComparator : undefined;
|
|
262
|
+
const c = comparator
|
|
263
|
+
? applySortDirection(callSortComparator(
|
|
264
|
+
comparator,
|
|
265
|
+
{ id: a.row.id, field: item.field, row: a.row, value: va },
|
|
266
|
+
{ id: b.row.id, field: item.field, row: b.row, value: vb },
|
|
267
|
+
), item.sort)
|
|
268
|
+
: compareValues(va, vb, item.sort, typeOf(item.field));
|
|
260
269
|
if (c !== 0) return c;
|
|
261
270
|
}
|
|
262
271
|
return 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import type { GridColDef, GridRowModel, GridSortDirection, GridSortItem } from '../../types';
|
|
2
|
+
import type { GridColDef, GridRowId, GridRowModel, GridSortCellParams, GridSortDirection, GridSortItem } from '../../types';
|
|
3
3
|
import type { GridColumnLookup } from '../columnLookup';
|
|
4
|
-
import { getCellValue, toDate, toNumber } from '../values';
|
|
4
|
+
import { getCellValue, toDate, toNumber, warnCallbackFailure } from '../values';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Language-aware string order: accented letters sort next to their base letter instead of after
|
|
@@ -68,7 +68,8 @@ function compareSortKeys(a: SortKey, b: SortKey): number {
|
|
|
68
68
|
return a.num < b.num ? -1 : a.num > b.num ? 1 : 0;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
/** Reverse an ascending comparison for 'desc'. */
|
|
72
|
+
export function applySortDirection(comparison: number, direction: 'asc' | 'desc'): number {
|
|
72
73
|
if (comparison === 0) return 0;
|
|
73
74
|
return direction === 'asc' ? comparison : -comparison;
|
|
74
75
|
}
|
|
@@ -83,27 +84,62 @@ export function compareValues(
|
|
|
83
84
|
direction: 'asc' | 'desc',
|
|
84
85
|
type?: GridColDef['type']
|
|
85
86
|
): number {
|
|
86
|
-
return
|
|
87
|
+
return applySortDirection(compareSortKeys(toSortKey(a, type), toSortKey(b, type)), direction);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** A column's `sortComparator`, as the sorting utils call it. */
|
|
91
|
+
export type GridSortComparatorFn = NonNullable<GridColDef['sortComparator']>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Call a consumer `sortComparator`, containing failures: a throw or a non-finite result reads as 0
|
|
95
|
+
* (equal), with a one-time dev warning per column, so one bad comparison cannot break the sort.
|
|
96
|
+
*/
|
|
97
|
+
export function callSortComparator(
|
|
98
|
+
comparator: GridSortComparatorFn,
|
|
99
|
+
a: GridSortCellParams,
|
|
100
|
+
b: GridSortCellParams
|
|
101
|
+
): number {
|
|
102
|
+
let result: number;
|
|
103
|
+
try {
|
|
104
|
+
result = comparator(a.value, b.value, a, b);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
warnCallbackFailure(`sortComparator:${a.field}`, `sortComparator for column "${a.field}" threw; the values are treated as equal`, error);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
if (typeof result !== 'number' || Number.isNaN(result)) {
|
|
110
|
+
warnCallbackFailure(`sortComparator:${a.field}:nan`, `sortComparator for column "${a.field}" returned ${String(result)}; the values are treated as equal`, undefined);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const defaultRowId = (row: GridRowModel): GridRowId => row.id;
|
|
117
|
+
|
|
89
118
|
/**
|
|
90
119
|
* Compare two rows by a sort model, reading cells through `valueGetter` and using each column's
|
|
91
|
-
* `type
|
|
120
|
+
* `type` (or its `sortComparator`). Returns 0 when every key is equal.
|
|
92
121
|
*/
|
|
93
122
|
export function compareRowsBySortModel(
|
|
94
123
|
a: GridRowModel,
|
|
95
124
|
b: GridRowModel,
|
|
96
125
|
sortModel: readonly GridSortItem[],
|
|
97
|
-
columns?: GridColumnLookup
|
|
126
|
+
columns?: GridColumnLookup,
|
|
127
|
+
getRowId: (row: GridRowModel) => GridRowId = defaultRowId
|
|
98
128
|
): number {
|
|
99
129
|
for (const sortItem of sortModel) {
|
|
100
130
|
const colDef = columns?.byField.get(sortItem.field);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
const va = getCellValue(a, sortItem.field, colDef);
|
|
132
|
+
const vb = getCellValue(b, sortItem.field, colDef);
|
|
133
|
+
const comparison = colDef?.sortComparator
|
|
134
|
+
? applySortDirection(
|
|
135
|
+
callSortComparator(
|
|
136
|
+
colDef.sortComparator,
|
|
137
|
+
{ id: getRowId(a), field: sortItem.field, row: a, value: va },
|
|
138
|
+
{ id: getRowId(b), field: sortItem.field, row: b, value: vb }
|
|
139
|
+
),
|
|
140
|
+
sortItem.sort
|
|
141
|
+
)
|
|
142
|
+
: compareValues(va, vb, sortItem.sort, colDef?.type);
|
|
107
143
|
if (comparison !== 0) return comparison;
|
|
108
144
|
}
|
|
109
145
|
return 0;
|
|
@@ -112,28 +148,42 @@ export function compareRowsBySortModel(
|
|
|
112
148
|
export function sortRows<R extends GridRowModel>(
|
|
113
149
|
rows: R[],
|
|
114
150
|
sortModel: GridSortItem[],
|
|
115
|
-
columns?: GridColumnLookup
|
|
151
|
+
columns?: GridColumnLookup,
|
|
152
|
+
getRowId: (row: R) => GridRowId = defaultRowId
|
|
116
153
|
): R[] {
|
|
117
154
|
if (sortModel.length === 0) {
|
|
118
155
|
return rows;
|
|
119
156
|
}
|
|
120
157
|
return sortItemsBySortModel(rows, sortModel, (row, sortItem) => {
|
|
121
158
|
const colDef = columns?.byField.get(sortItem.field);
|
|
122
|
-
|
|
159
|
+
const value = getCellValue(row, sortItem.field, colDef);
|
|
160
|
+
return {
|
|
161
|
+
value,
|
|
162
|
+
type: colDef?.type,
|
|
163
|
+
comparator: colDef?.sortComparator,
|
|
164
|
+
params: colDef?.sortComparator ? { id: getRowId(row), field: sortItem.field, row, value } : undefined,
|
|
165
|
+
};
|
|
123
166
|
});
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
/** The value an item sorts by for one sort key, and
|
|
169
|
+
/** The value an item sorts by for one sort key, and how to compare it. */
|
|
127
170
|
export interface GridSortValue {
|
|
128
171
|
value: unknown;
|
|
129
172
|
type?: GridColDef['type'];
|
|
173
|
+
/**
|
|
174
|
+
* The column's `sortComparator`. Two items are compared with it when both carry the same one
|
|
175
|
+
* (with their `params`); otherwise the built-in comparison of `value` / `type` applies.
|
|
176
|
+
*/
|
|
177
|
+
comparator?: GridSortComparatorFn;
|
|
178
|
+
/** The params passed to `comparator` for this item; `value` here is what the comparator sees. */
|
|
179
|
+
params?: GridSortCellParams;
|
|
130
180
|
}
|
|
131
181
|
|
|
132
182
|
/**
|
|
133
|
-
* Stable sort of any items by a sort model. `readValue` gives the value (and column type
|
|
134
|
-
* sorts by for each sort key; it is called once per item and key, not once per
|
|
135
|
-
* the flat pipeline and by tree data / row grouping, whose synthetic group rows
|
|
136
|
-
* grouping value or label instead of a cell.
|
|
183
|
+
* Stable sort of any items by a sort model. `readValue` gives the value (and column type or
|
|
184
|
+
* comparator) an item sorts by for each sort key; it is called once per item and key, not once per
|
|
185
|
+
* comparison. Used by the flat pipeline and by tree data / row grouping, whose synthetic group rows
|
|
186
|
+
* sort by their grouping value or label instead of a cell.
|
|
137
187
|
*/
|
|
138
188
|
export function sortItemsBySortModel<T>(
|
|
139
189
|
items: readonly T[],
|
|
@@ -146,15 +196,18 @@ export function sortItemsBySortModel<T>(
|
|
|
146
196
|
|
|
147
197
|
// Read and normalize every sort value once, instead of calling valueGetter and parsing dates
|
|
148
198
|
// O(n log n) times inside the comparator.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
return toSortKey(value, type);
|
|
152
|
-
}));
|
|
199
|
+
const values = sortModel.map(sortItem => items.map(item => readValue(item, sortItem)));
|
|
200
|
+
const keys = values.map(column => column.map(({ value, type }) => toSortKey(value, type)));
|
|
153
201
|
|
|
154
202
|
const order = items.map((_, index) => index);
|
|
155
203
|
order.sort((ia, ib) => {
|
|
156
204
|
for (let k = 0; k < sortModel.length; k++) {
|
|
157
|
-
const
|
|
205
|
+
const a = values[k][ia];
|
|
206
|
+
const b = values[k][ib];
|
|
207
|
+
const ascending = a.comparator && a.comparator === b.comparator && a.params && b.params
|
|
208
|
+
? callSortComparator(a.comparator, a.params, b.params)
|
|
209
|
+
: compareSortKeys(keys[k][ia], keys[k][ib]);
|
|
210
|
+
const comparison = applySortDirection(ascending, sortModel[k].sort);
|
|
158
211
|
if (comparison !== 0) return comparison;
|
|
159
212
|
}
|
|
160
213
|
return ia - ib;
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenGridX — AI Agent Context
|
|
2
2
|
# Package: @opencorestack/opengridx
|
|
3
|
-
# Version: 3.0
|
|
3
|
+
# Version: 3.2.0
|
|
4
4
|
# Docs: https://opencorestack.github.io/OpenGridX/
|
|
5
5
|
# This file is machine-readable. AI developers: start here.
|
|
6
6
|
|
|
@@ -248,8 +248,9 @@ There are no getRowClassName / getCellClassName / theme props: use GridColDef.ce
|
|
|
248
248
|
| align | 'left'/'center'/'right' | Cell content alignment |
|
|
249
249
|
| headerAlign | 'left'/'center'/'right' | Header content alignment |
|
|
250
250
|
| sortable | boolean (default true) | Enable sorting |
|
|
251
|
+
| sortComparator | (v1, v2, params1, params2) => number | Custom ascending comparator (3.1.0+). v1/v2 are valueGetter values incl. null/undefined; params { id, field, row, value }. Negated for desc; multi-sort tie-break chain; groups by grouping value; tree siblings; pivot row labels. Throw/NaN = 0. Ignored in server sortingMode |
|
|
251
252
|
| filterable | boolean (default true) | Enable filtering and quick search |
|
|
252
|
-
| resizable | boolean (default true) | Enable resize handle (mouse, touch, Alt+Arrow on header) |
|
|
253
|
+
| resizable | boolean (default true) | Enable resize handle (mouse, touch, Alt+Arrow on header); double-click or Enter on the handle auto-sizes to content (3.1.0+) |
|
|
253
254
|
| hideable | boolean (default true) | Allow hiding via toolbar / column menu |
|
|
254
255
|
| pinnable | boolean (default true) | Allow pinning via column menu (does not affect drag-reorder) |
|
|
255
256
|
| exportable | boolean (default true) | Include in CSV/Excel/JSON/Print/PDF exports and clipboard copy |
|
|
@@ -298,6 +299,10 @@ apiRef.current.setPageSize(size)
|
|
|
298
299
|
// Scroll
|
|
299
300
|
apiRef.current.scrollToIndexes({ rowIndex, colIndex })
|
|
300
301
|
|
|
302
|
+
// Auto-size to header + rendered cells, clamped to minWidth/maxWidth (3.1.0+)
|
|
303
|
+
apiRef.current.autosizeColumn(field)
|
|
304
|
+
apiRef.current.autosizeColumns(fields?)
|
|
305
|
+
|
|
301
306
|
// Clipboard
|
|
302
307
|
apiRef.current.copySelectedRows() // → Promise<void>; rejects if the write fails. Copies every selected row that passes
|
|
303
308
|
// the filter (all pages, collapsed groups, pinned rows), visible columns in screen order
|
|
@@ -492,10 +497,13 @@ await exportToExcelAdvanced(rows, columns, {
|
|
|
492
497
|
});
|
|
493
498
|
exportToJson(rows, columns, { fileName: 'export.json' });
|
|
494
499
|
await exportToPdf(rows, columns, { fileName: 'export', title: 'My Report' });
|
|
500
|
+
// 3.2.0+: large PDFs — progress, cancel, size warning (never throws)
|
|
501
|
+
await exportToPdf(rows, columns, { onProgress: ({ phase, done, total }) => {}, signal: controller.signal, maxRows: 20000 });
|
|
495
502
|
await printGrid(rows, columns, 'My Report Title');
|
|
496
503
|
```
|
|
497
504
|
|
|
498
505
|
- Every export accepts `selectedRows`, `getRowId` (pass the grid's getRowId so ids match), `aggregationResult` + `aggregationModel` (totals row) and `groupedRows` from `apiRef.current.getGroupedExportRows()`.
|
|
506
|
+
- exportToPdf (3.2.0+) works in ~30 ms slices and yields between them (prepare and render phases), so the page stays responsive; only jsPDF's final save is one synchronous step. `onProgress({ phase: 'prepare'|'render'|'save', done, total })`; `signal` abort rejects with DOMException 'AbortError' and saves nothing; above `maxRows` (default 20000) a dev console.warn recommends exportToCsv / exportToExcelAdvanced. Output (pages, positions) is the same as before.
|
|
499
507
|
- CSV and exportToExcel neutralise formulas: text starting with = + - @ tab or CR gets a leading `'` (`escapeFormulas: false` opts out).
|
|
500
508
|
- exportToExcel writes HTML: a `.xlsx` fileName is saved as `.xls` with a console warning.
|
|
501
509
|
- Columns with `exportable: false` are left out everywhere; `valueGetter` and `valueFormatter` are applied.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.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",
|