@opencorestack/opengridx 2.0.4 → 2.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +45 -3
  2. package/dist/index.d.ts +43 -2
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +3927 -3839
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +13 -4
  9. package/docs/components/column-visibility.md +40 -16
  10. package/docs/customization/slots-api.md +18 -6
  11. package/docs/features/export-guide.md +22 -3
  12. package/docs/features/sorting-pagination.md +2 -0
  13. package/docs/features/tree-data-grouping.md +8 -1
  14. package/docs/features/virtualization.md +36 -8
  15. package/docs/roadmap.md +13 -7
  16. package/lib/components/Cell/Cell.tsx +1 -0
  17. package/lib/components/DataGrid/DataGrid.css +7 -0
  18. package/lib/components/DataGrid/DataGrid.defects.test.tsx +103 -0
  19. package/lib/components/DataGrid/DataGrid.tsx +42 -10
  20. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +183 -0
  21. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  22. package/lib/components/DataGrid/GridListView.tsx +3 -0
  23. package/lib/components/DataGrid/GridPinnedRows.tsx +3 -0
  24. package/lib/components/DataGrid/GridVirtualRows.tsx +8 -1
  25. package/lib/components/ListView/ListViewRow.tsx +3 -0
  26. package/lib/components/Row/Row.tsx +10 -0
  27. package/lib/hooks/core/useGridColumns.tsx +8 -3
  28. package/lib/hooks/core/useGridDevWarnings.ts +42 -0
  29. package/lib/hooks/core/useGridKeyboardNavigation.ts +12 -14
  30. package/lib/hooks/core/useGridVisibleRows.ts +3 -2
  31. package/lib/hooks/features/useAggregation.ts +3 -38
  32. package/lib/hooks/useRowGrouping.test.ts +162 -1
  33. package/lib/hooks/useRowGrouping.ts +42 -59
  34. package/lib/hooks/useTreeData.test.ts +83 -0
  35. package/lib/hooks/useTreeData.ts +44 -43
  36. package/lib/index.ts +2 -0
  37. package/lib/types/index.ts +15 -2
  38. package/lib/utils/aggregation/aggregation.test.ts +16 -0
  39. package/lib/utils/aggregation/index.ts +62 -0
  40. package/lib/utils/export/exportToExcelAdvanced.test.ts +109 -0
  41. package/lib/utils/export/exportToExcelAdvanced.ts +89 -5
  42. package/lib/utils/scroll/index.ts +28 -0
  43. package/llms.txt +22 -13
  44. package/package.json +7 -3
@@ -0,0 +1,62 @@
1
+ import type { GridAggregationModel, GridAggregationResult, GridColDef, GridRowModel } from '../../types';
2
+
3
+ export type BuiltInAggFn = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'unique';
4
+
5
+ const toNumbers = (values: unknown[]): number[] => {
6
+ const nums: number[] = [];
7
+ for (const v of values) {
8
+ if (v == null) continue;
9
+ const n = Number(v);
10
+ if (!isNaN(n)) nums.push(n);
11
+ }
12
+ return nums;
13
+ };
14
+
15
+ // Loops instead of Math.min(...nums): spreading >~120k arguments throws RangeError.
16
+ const minOf = (nums: number[]): number | null => {
17
+ if (nums.length === 0) return null;
18
+ let m = nums[0];
19
+ for (let i = 1; i < nums.length; i++) if (nums[i] < m) m = nums[i];
20
+ return m;
21
+ };
22
+
23
+ const maxOf = (nums: number[]): number | null => {
24
+ if (nums.length === 0) return null;
25
+ let m = nums[0];
26
+ for (let i = 1; i < nums.length; i++) if (nums[i] > m) m = nums[i];
27
+ return m;
28
+ };
29
+
30
+ export const AGGREGATION_FUNCTIONS: Record<BuiltInAggFn, (values: unknown[]) => unknown> = {
31
+ sum: (values) => toNumbers(values).reduce((acc, n) => acc + n, 0),
32
+ avg: (values) => {
33
+ const nums = toNumbers(values);
34
+ return nums.length ? nums.reduce((acc, n) => acc + n, 0) / nums.length : null;
35
+ },
36
+ count: (values) => values.filter((v) => v != null).length,
37
+ min: (values) => minOf(toNumbers(values)),
38
+ max: (values) => maxOf(toNumbers(values)),
39
+ unique: (values) => new Set(values.filter((v) => v != null)).size,
40
+ };
41
+
42
+ export function computeAggregations<R extends GridRowModel>(
43
+ rows: R[],
44
+ aggregationModel: GridAggregationModel,
45
+ columnsLookup: Map<string, GridColDef<R>>,
46
+ warnPrefix: string,
47
+ ): GridAggregationResult {
48
+ const result: GridAggregationResult = {};
49
+ for (const [field, fnName] of Object.entries(aggregationModel)) {
50
+ const colDef = columnsLookup.get(field);
51
+ if (colDef?.availableAggregationFunctions && !colDef.availableAggregationFunctions.includes(fnName)) {
52
+ continue;
53
+ }
54
+ const fn = AGGREGATION_FUNCTIONS[fnName as BuiltInAggFn];
55
+ if (!fn) {
56
+ console.warn(`[${warnPrefix}] Unknown aggregation function: "${fnName}"`);
57
+ continue;
58
+ }
59
+ result[field] = fn(rows.map((row) => row[field]));
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import ExcelJS from 'exceljs';
3
+ import { exportToExcelAdvanced } from './exportToExcelAdvanced';
4
+ import type { GridColDef, GridGroupedExportRow, GridRowModel } from '../../types';
5
+
6
+ type Row = GridRowModel & { id: number; dept: string; salary: number };
7
+
8
+ const columns: GridColDef<Row>[] = [
9
+ { field: 'dept', headerName: 'Department' },
10
+ { field: 'salary', headerName: 'Salary', type: 'number' },
11
+ ];
12
+
13
+ const flatRows: Row[] = [
14
+ { id: 1, dept: 'Engineering', salary: 90000 },
15
+ { id: 2, dept: 'Engineering', salary: 85000 },
16
+ { id: 3, dept: 'Marketing', salary: 70000 },
17
+ ];
18
+
19
+ const groupedRows: GridGroupedExportRow[] = [
20
+ { type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Engineering' },
21
+ { type: 'leaf', depth: 1, row: flatRows[0] },
22
+ { type: 'leaf', depth: 1, row: flatRows[1] },
23
+ { type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Engineering', aggregatedValues: { salary: 175000 } },
24
+ { type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Marketing' },
25
+ { type: 'leaf', depth: 1, row: flatRows[2] },
26
+ { type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Marketing', aggregatedValues: { salary: 70000 } },
27
+ { type: 'grand-total', depth: 0, aggregatedValues: { salary: 245000 } },
28
+ ];
29
+
30
+ let capturedBlob: Blob | null = null;
31
+ const origCreateObjectURL = URL.createObjectURL;
32
+ const origRevokeObjectURL = URL.revokeObjectURL;
33
+ const origClick = HTMLAnchorElement.prototype.click;
34
+
35
+ beforeEach(() => {
36
+ capturedBlob = null;
37
+ URL.createObjectURL = (blob: Blob) => { capturedBlob = blob; return 'blob:mock'; };
38
+ URL.revokeObjectURL = () => {};
39
+ HTMLAnchorElement.prototype.click = () => {};
40
+ });
41
+
42
+ afterEach(() => {
43
+ URL.createObjectURL = origCreateObjectURL;
44
+ URL.revokeObjectURL = origRevokeObjectURL;
45
+ HTMLAnchorElement.prototype.click = origClick;
46
+ });
47
+
48
+ async function readSheet(): Promise<ExcelJS.Worksheet> {
49
+ const wb = new ExcelJS.Workbook();
50
+ await wb.xlsx.load(await capturedBlob!.arrayBuffer());
51
+ return wb.worksheets[0];
52
+ }
53
+
54
+ const rowValues = (ws: ExcelJS.Worksheet) =>
55
+ ws.getSheetValues().slice(1).map(r => (Array.isArray(r) ? r.slice(1) : []));
56
+
57
+ describe('exportToExcelAdvanced — flat', () => {
58
+ it('writes a header row and one row per record when groupedRows is absent', async () => {
59
+ await exportToExcelAdvanced(flatRows, columns);
60
+ const ws = await readSheet();
61
+ expect(rowValues(ws)).toEqual([
62
+ ['Department', 'Salary'],
63
+ ['Engineering', 90000],
64
+ ['Engineering', 85000],
65
+ ['Marketing', 70000],
66
+ ]);
67
+ });
68
+ });
69
+
70
+ describe('exportToExcelAdvanced — grouped', () => {
71
+ it('writes group headers, leaves, subtotals and a single grand total in order', async () => {
72
+ await exportToExcelAdvanced(flatRows, columns, {
73
+ groupedRows,
74
+ aggregationModel: { salary: 'sum' },
75
+ aggregationResult: { salary: 245000 },
76
+ sheets: [{ name: 'Data', includeSummary: true }],
77
+ });
78
+ const ws = await readSheet();
79
+ expect(rowValues(ws)).toEqual([
80
+ ['Department', 'Salary'],
81
+ ['Department: Engineering'],
82
+ ['Engineering', 90000],
83
+ ['Engineering', 85000],
84
+ ['Subtotal', 175000],
85
+ ['Department: Marketing'],
86
+ ['Marketing', 70000],
87
+ ['Subtotal', 70000],
88
+ ['Grand Total', 245000],
89
+ ]);
90
+ });
91
+
92
+ it('outlines leaf rows one level below their group header', async () => {
93
+ await exportToExcelAdvanced(flatRows, columns, { groupedRows });
94
+ const ws = await readSheet();
95
+ expect(ws.getRow(2).outlineLevel ?? 0).toBe(0);
96
+ expect(ws.getRow(3).outlineLevel).toBe(1);
97
+ expect(ws.getRow(4).outlineLevel).toBe(1);
98
+ });
99
+
100
+ it('uses groupingValueFormatter for group-header labels', async () => {
101
+ const cols: GridColDef<Row>[] = [
102
+ { ...columns[0], groupingValueFormatter: ({ value }) => `Dept ${String(value)}` },
103
+ columns[1],
104
+ ];
105
+ await exportToExcelAdvanced(flatRows, cols, { groupedRows });
106
+ const ws = await readSheet();
107
+ expect(ws.getRow(2).getCell(1).value).toBe('Dept Engineering');
108
+ });
109
+ });
@@ -1,5 +1,5 @@
1
1
 
2
- import type { GridColDef, GridRowModel, GridAggregationModel } from '../../types';
2
+ import type { GridColDef, GridRowModel, GridAggregationModel, GridGroupedExportRow } from '../../types';
3
3
  import { formatAggregationValue } from '../../hooks/features/useAggregation';
4
4
 
5
5
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -68,6 +68,19 @@ export interface ExcelAdvancedExportOptions {
68
68
  aggregationModel?: GridAggregationModel | null;
69
69
  /** Currently selected row IDs */
70
70
  selectedRows?: (string | number)[];
71
+ /**
72
+ * Grouped row structure, typically from `apiRef.current.getGroupedExportRows()`.
73
+ * When provided, sheets with `rows: 'all'` are written in grouped order: group-header,
74
+ * leaf, group-subtotal and grand-total rows, using Excel row outlining so groups
75
+ * collapse natively. A `grand-total` entry replaces the sheet's `includeSummary` row.
76
+ * Ignored for `rows: 'selected'` sheets.
77
+ * @since v2.1
78
+ */
79
+ groupedRows?: GridGroupedExportRow[];
80
+ /** Fill color for group-header rows in grouped export (default: '#e8eaf6') */
81
+ groupHeaderFillColor?: string;
82
+ /** Fill color for group-subtotal rows in grouped export (default: '#f0f4ff') */
83
+ groupSubtotalFillColor?: string;
71
84
  }
72
85
 
73
86
  // ─── Cell type inference ──────────────────────────────────────────────────────
@@ -187,6 +200,9 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
187
200
  aggregationResult = null,
188
201
  aggregationModel = null,
189
202
  selectedRows,
203
+ groupedRows,
204
+ groupHeaderFillColor = '#e8eaf6',
205
+ groupSubtotalFillColor = '#f0f4ff',
190
206
  } = options;
191
207
 
192
208
  // Lazy-load ExcelJS to keep initial bundle lean
@@ -270,7 +286,12 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
270
286
  alternateRowColor = '#f8fafc',
271
287
  } = def;
272
288
 
273
- const rowsToExport = rowScope === 'selected' ? sRows : allRows;
289
+ const useGrouped = rowScope === 'all' && Boolean(groupedRows && groupedRows.length > 0);
290
+ const rowsToExport = rowScope === 'selected'
291
+ ? sRows
292
+ : useGrouped
293
+ ? groupedRows!.flatMap(e => (e.type === 'leaf' && e.row ? [e.row] : []))
294
+ : allRows;
274
295
 
275
296
  // ─ Pre-fetch images for this sheet ──────────────────────────────────────────
276
297
  const imageColumns = exportColumns.filter(col => columnStyles[col.field]?.embedImage);
@@ -343,7 +364,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
343
364
  // Header occupies index 0 when present; first data row is at index 1.
344
365
  let excelRowIdx = includeHeaders ? 1 : 0;
345
366
 
346
- rowsToExport.forEach((row, rowIdx) => {
367
+ const writeDataRow = (row: GridRowModel, rowIdx: number, outlineLevel: number) => {
347
368
  const cellValues = exportColumns.map(col => {
348
369
  // Image columns: write empty string; image placed below
349
370
  if (columnStyles[col.field]?.embedImage) return '';
@@ -361,6 +382,7 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
361
382
 
362
383
  const dataRow = ws.addRow(cellValues);
363
384
  dataRow.font = { size: bodyFontSize };
385
+ if (outlineLevel > 0) dataRow.outlineLevel = outlineLevel;
364
386
 
365
387
  // Alternate row background
366
388
  if (alternateRowColor && rowIdx % 2 === 1) {
@@ -440,10 +462,72 @@ export async function exportToExcelAdvanced<R extends GridRowModel>(
440
462
  }
441
463
 
442
464
  excelRowIdx++;
443
- });
465
+ };
466
+
467
+ const groupLabel = (entry: GridGroupedExportRow): string => {
468
+ const colDef = exportColumns.find(c => c.field === entry.groupField);
469
+ if (colDef?.groupingValueFormatter && entry.groupField) {
470
+ return colDef.groupingValueFormatter({ field: entry.groupField, value: entry.groupValue });
471
+ }
472
+ return `${colDef?.headerName ?? entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
473
+ };
474
+
475
+ // Numbers are written raw so the column numFmt applies; other values use the formatter.
476
+ const aggregateCell = (col: GridColDef<GridRowModel>, values: Record<string, unknown>): unknown => {
477
+ const val = values[col.field];
478
+ if (val == null) return '';
479
+ if (typeof val === 'number') return val;
480
+ if (col.valueFormatter) return col.valueFormatter({ value: val, row: {} as GridRowModel, field: col.field });
481
+ return formatAggregationValue(val, aggregationModel?.[col.field] ?? '');
482
+ };
483
+
484
+ const writeSummaryRow = (label: string, values: Record<string, unknown>, fill: string, outlineLevel: number, bold: boolean) => {
485
+ const cells = exportColumns.map((col, i) => (i === 0 && values[col.field] == null ? label : aggregateCell(col, values)));
486
+ const summaryRow = ws.addRow(cells);
487
+ summaryRow.height = 18;
488
+ if (outlineLevel > 0) summaryRow.outlineLevel = outlineLevel;
489
+ summaryRow.font = { bold, italic: !bold, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
490
+ summaryRow.eachCell((cell, colIndex) => {
491
+ cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(fill) } };
492
+ const colDef = exportColumns[colIndex - 1];
493
+ cell.alignment = { horizontal: colIndex === 1 ? 'left' : (colDef?.type === 'number' ? 'right' : 'left'), vertical: 'middle' };
494
+ cell.border = topBorder();
495
+ });
496
+ excelRowIdx++;
497
+ };
498
+
499
+ let wroteGrandTotal = false;
500
+
501
+ if (useGrouped) {
502
+ let stripe = 0;
503
+ groupedRows!.forEach(entry => {
504
+ if (entry.type === 'group-header') {
505
+ const groupHeaderRow = ws.addRow([groupLabel(entry)]);
506
+ groupHeaderRow.height = 18;
507
+ if (entry.depth > 0) groupHeaderRow.outlineLevel = entry.depth;
508
+ groupHeaderRow.font = { bold: true, size: bodyFontSize, color: { argb: argb(headerTextColor) } };
509
+ for (let c = 1; c <= exportColumns.length; c++) {
510
+ groupHeaderRow.getCell(c).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: argb(groupHeaderFillColor) } };
511
+ }
512
+ groupHeaderRow.getCell(1).alignment = { horizontal: 'left', vertical: 'middle', indent: entry.depth };
513
+ excelRowIdx++;
514
+ stripe = 0;
515
+ } else if (entry.type === 'leaf' && entry.row) {
516
+ writeDataRow(entry.row, stripe++, entry.depth);
517
+ } else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
518
+ writeSummaryRow('Subtotal', entry.aggregatedValues, groupSubtotalFillColor, entry.depth, false);
519
+ } else if (entry.type === 'grand-total' && entry.aggregatedValues) {
520
+ writeSummaryRow('Grand Total', entry.aggregatedValues, headerFillColor, 0, true);
521
+ wroteGrandTotal = true;
522
+ }
523
+ });
524
+ ws.properties.outlineProperties = { summaryBelow: true, summaryRight: false };
525
+ } else {
526
+ rowsToExport.forEach((row, rowIdx) => writeDataRow(row, rowIdx, 0));
527
+ }
444
528
 
445
529
  // ── Aggregation totals row ────────────────────────────────────────────
446
- if (includeSummary && aggregationResult && aggregationModel) {
530
+ if (includeSummary && !wroteGrandTotal && aggregationResult && aggregationModel) {
447
531
  // Label row
448
532
  const labelValues = exportColumns.map(col => {
449
533
  const fn = aggregationModel![col.field];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Vertically scrolls the grid viewport so the center (unpinned) row at `centerIndex`
3
+ * is fully visible below the sticky header / top-pinned rows and above the
4
+ * bottom-pinned rows. `cumulativeHeights` is the unpinned-row layout.
5
+ */
6
+ export function scrollRowIntoView(
7
+ viewport: HTMLElement,
8
+ centerIndex: number,
9
+ cumulativeHeights: number[],
10
+ pinnedBottomHeight: number,
11
+ ): void {
12
+ if (centerIndex < 0 || centerIndex >= cumulativeHeights.length) return;
13
+ const virtualContainer = viewport.querySelector<HTMLElement>('.ogx__virtual-container');
14
+ if (!virtualContainer) return;
15
+
16
+ const { scrollTop, clientHeight } = viewport;
17
+ // Everything above the center rows is sticky, so it covers exactly this many
18
+ // pixels at the top of the viewport at any scroll position.
19
+ const stickyTop = virtualContainer.getBoundingClientRect().top - viewport.getBoundingClientRect().top + scrollTop;
20
+ const rowTop = stickyTop + (centerIndex === 0 ? 0 : cumulativeHeights[centerIndex - 1]);
21
+ const rowBottom = stickyTop + cumulativeHeights[centerIndex];
22
+
23
+ if (rowTop < scrollTop + stickyTop) {
24
+ viewport.scrollTop = Math.max(0, rowTop - stickyTop);
25
+ } else if (rowBottom > scrollTop + clientHeight - pinnedBottomHeight) {
26
+ viewport.scrollTop = rowBottom - clientHeight + pinnedBottomHeight;
27
+ }
28
+ }
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenGridX — AI Agent Context
2
2
  # Package: @opencorestack/opengridx
3
- # Version: 2.0.4
3
+ # Version: 2.1.0
4
4
  # Docs: https://opencorestack.github.io/OpenGridX/
5
5
  # This file is machine-readable. AI developers: start here.
6
6
 
@@ -67,8 +67,11 @@ export default function App() {
67
67
  ### Display
68
68
  | Prop | Type | Default | Description |
69
69
  |-------------------|-----------|---------|----------------------------------------|
70
- | height | number | 500 | Grid height in pixels |
70
+ | height | number/string | — | Grid height. The grid must have a BOUNDED height or it renders every row (see GOTCHAS) |
71
71
  | autoHeight | boolean | false | Fit height to rows |
72
+ | overscanRowCount | number | 3 | Minimum rows rendered outside the viewport (adaptive) |
73
+ | ariaLabel | string | — | Accessible name for the grid |
74
+ | localeText | GridLocaleText | — | Override pagination / no-rows strings |
72
75
  | rowHeight | number | 52 | Row height in pixels |
73
76
  | headerHeight | number | 56 | Header height in pixels |
74
77
  | loading | boolean | false | Show skeleton loading overlay |
@@ -79,6 +82,8 @@ export default function App() {
79
82
  | Prop | Type | Description |
80
83
  |----------------------------|-------------------------------|-------------------------------------|
81
84
  | checkboxSelection | boolean | Enable checkbox column |
85
+ | disableMultipleRowSelection | boolean | Cap selection to one row |
86
+ | pinCheckboxColumn | boolean (default true) | Pin the checkbox column left |
82
87
  | rowSelectionModel | GridRowId[] | Controlled selection |
83
88
  | onRowSelectionModelChange | (model: GridRowId[]) => void | Selection change callback |
84
89
  | disableRowSelectionOnClick | boolean | Prevent click toggling selection |
@@ -87,9 +92,9 @@ export default function App() {
87
92
  | Prop | Type | Description |
88
93
  |-------------------|-----------------------------|----------------------------------|
89
94
  | sortModel | GridSortItem[] | Controlled sort state |
95
+ | multiSort | boolean | Plain click appends to sortModel (Shift+click always does) |
90
96
  | onSortModelChange | (model: GridSortItem[]) => void | Sort change callback |
91
97
  | sortingMode | 'client' / 'server' | Where sorting is applied |
92
- | disableColumnSorting | boolean | Disable sorting for all columns |
93
98
 
94
99
  ### Filtering
95
100
  | Prop | Type | Description |
@@ -97,7 +102,6 @@ export default function App() {
97
102
  | filterModel | GridFilterModel | Controlled filter state |
98
103
  | onFilterModelChange | (model: GridFilterModel) => void | Filter change callback |
99
104
  | filterMode | 'client' / 'server' | Where filtering is applied |
100
- | disableColumnFilter | boolean | Disable filter for all columns |
101
105
 
102
106
  ### Pagination
103
107
  | Prop | Type | Description |
@@ -119,7 +123,6 @@ export default function App() {
119
123
  | columnOrder | string[] | Controlled column order |
120
124
  | onColumnOrderChange | (params) => void | Column reorder callback |
121
125
  | disableColumnReorder | boolean | Disable drag-to-reorder |
122
- | disableColumnResize | boolean | Disable resize handles |
123
126
  | columnGroupingModel | GridColumnGroupingModel | Multi-level column header groups |
124
127
 
125
128
  ### Row Features
@@ -129,8 +132,6 @@ export default function App() {
129
132
  | rowReordering | boolean | Enable drag-and-drop row reordering |
130
133
  | onRowOrderChange | (params) => void | Row reorder callback |
131
134
  | getRowId | (row: R) => GridRowId | Custom row ID accessor (default: row.id) |
132
- | getRowClassName | (params) => string | Dynamic CSS class per row |
133
- | getRowHeight | (params) => number | Dynamic row height |
134
135
 
135
136
  ### Cell Editing
136
137
  | Prop | Type | Description |
@@ -143,15 +144,18 @@ export default function App() {
143
144
  | Prop | Type | Description |
144
145
  |-------------------------|----------------------------|-----------------------------------------|
145
146
  | getDetailPanelContent | (params) => ReactNode | Render expanded detail row |
147
+ | pinExpandColumn | boolean (default true) | Pin the detail-panel expand column |
146
148
  | getDetailPanelHeight | (params) => number / 'auto'| Height of detail panel |
147
- | expandedRowIds | Set<GridRowId> | Controlled expanded rows |
149
+ | detailPanelExpandedRowIds | Set<GridRowId> | Controlled expanded rows |
148
150
  | onDetailPanelExpandedRowIdsChange | (ids) => void | Expand/collapse callback |
149
151
 
150
152
  ### Grouping & Aggregation
151
153
  | Prop | Type | Description |
152
154
  |------------------------|---------------------------|------------------------------------------|
153
- | rowGroupingModel | string[] | Fields to group by |
155
+ | rowGroupingModel | string[] | Fields to group by (one nesting level per field). Disables pagination |
154
156
  | aggregationModel | GridAggregationModel | {field: 'sum'/'avg'/'min'/'max'/'count'} |
157
+ | onAggregationModelChange | (model) => void | Aggregation model changed |
158
+ | groupingColDef | Partial<GridColDef> | Dedicated pinned __group__ column |
155
159
  | getAggregationPosition | (group) => 'footer'/'inline' | Where to render aggregation row |
156
160
  | defaultGroupingExpansionDepth | number | Auto-expand depth (default -1 = all) |
157
161
 
@@ -179,8 +183,8 @@ export default function App() {
179
183
  |--------------|-----------------------|------------------------------------------|
180
184
  | slots | GridSlots | Replace toolbar, pagination, overlays |
181
185
  | slotProps | GridSlotProps | Props for slot components |
182
- | theme | GridTheme | Override theme tokens |
183
- | sx | React.CSSProperties | Inline styles on the root element |
186
+ | className | string | Extra CSS class on the root element |
187
+ | style | React.CSSProperties | Inline styles on the root element (use DataGridThemeProvider for theming) |
184
188
 
185
189
  ### Callbacks
186
190
  | Prop | Type | Description |
@@ -473,8 +477,9 @@ interface GridColumnPinning { left?: string[]; right?: string[]; }
473
477
  interface GridRowPinning { top?: GridRowId[]; bottom?: GridRowId[]; }
474
478
 
475
479
  interface GridRenderCellParams<R = GridRowModel> {
476
- value: any; row: R; field: string;
480
+ value: unknown; formattedValue?: string; row: R; field: string;
477
481
  colDef: GridColDef<R>; rowIndex: number; colIndex: number;
482
+ rowMeta?: GridRowMeta; // hierarchy info for tree-data / grouping rows
478
483
  }
479
484
 
480
485
  interface GridDataSource {
@@ -501,4 +506,8 @@ interface GridGetRowsResponse { rows: GridRowModel[]; rowCount: number; }
501
506
  7. **`singleSelect` editing**: for dropdown editors in cells, set `type: 'singleSelect'` and provide `valueOptions`.
502
507
  8. **`initialState.columns.columnWidths`**: accepts `Record<string, number>` to restore saved column widths.
503
508
  9. **`pinnedColumns`** and **`columnWidths`** are independent state — pinning does not reset widths.
504
- 10. **Aggregation** requires `rowGroupingModel` OR a global footer via `getAggregationPosition={() => 'footer'}`.
509
+ 10. **Aggregation**: setting `aggregationModel` alone renders a sticky footer with the totals; with `rowGroupingModel` each group row also shows its own aggregates. `null`/`undefined` values are ignored by every function.
510
+ 11. **GOTCHAS — bounded height**: virtualization only works if the grid's container has a bounded height. A flex child without `min-height: 0` grows to fit every row, so every row is rendered. Pagination hides this, and row grouping turns pagination off. Use `<div style={{ flex: 1, minHeight: 0 }}>` around `height="100%"`, or pass a fixed pixel `height`. Dev builds log a warning when this happens.
511
+ 12. **Pagination is ignored under row grouping** — a grouped grid is one scrollable, virtualized list.
512
+ 13. **`valueFormatter` + `renderCell`**: `renderCell` receives `params.formattedValue` (v2.1+), so a custom renderer can reuse the formatter instead of duplicating it.
513
+ 14. **`slots.footer`** replaces the pagination area and receives `aggregationResult`, `rowCount`, `paginationModel`, `onPaginationModelChange` and `apiRef` (v2.1+).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "2.0.4",
3
+ "version": "2.1.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",
@@ -41,8 +41,10 @@
41
41
  "build:lib": "tsc --project tsconfig.build.json && vite build --config vite.config.lib.js && cp lib/styles/opengridx.css.d.ts dist/opengridx.css.d.ts",
42
42
  "lint": "eslint .",
43
43
  "preview": "vite preview",
44
- "test": "vitest run",
45
- "test:watch": "vitest",
44
+ "test": "vitest run --project unit",
45
+ "test:browser": "vitest run --project browser",
46
+ "test:all": "vitest run",
47
+ "test:watch": "vitest --project unit",
46
48
  "coverage": "vitest run --coverage",
47
49
  "prepublishOnly": "npm run build:lib"
48
50
  },
@@ -103,6 +105,7 @@
103
105
  "@types/react": "^19.2.5",
104
106
  "@types/react-dom": "^19.2.3",
105
107
  "@vitejs/plugin-react": "^5.1.1",
108
+ "@vitest/browser-playwright": "^4.0.18",
106
109
  "@vitest/coverage-v8": "^4.0.18",
107
110
  "eslint": "^9.39.1",
108
111
  "eslint-plugin-react-hooks": "^7.0.1",
@@ -111,6 +114,7 @@
111
114
  "jsdom": "^29.1.1",
112
115
  "jspdf": "^4.2.1",
113
116
  "jspdf-autotable": "^5.0.8",
117
+ "playwright": "^1.58.2",
114
118
  "react": "^19.2.0",
115
119
  "react-dom": "^19.2.0",
116
120
  "react-router-dom": "^7.13.1",