@opencorestack/opengridx 1.2.3 → 2.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.
@@ -45,6 +45,7 @@ The main component for displaying and interacting with data.
45
45
  | :--- | :--- | :--- | :--- |
46
46
  | `sortingMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles sorting. The grid calls `onSortModelChange` but does not re-sort rows locally. |
47
47
  | `filterMode` | `'client' \| 'server'` | `'client'` | Set to `'server'` when your backend handles filtering. The grid calls `onFilterModelChange` but does not re-filter rows locally. |
48
+ | `multiSort` | `boolean` | `false` | When `true`, clicking any sortable column header appends/cycles it in the sort model instead of replacing it — no Shift key required. Shift+click always appends regardless of this prop. |
48
49
  | `onSortModelChange` | `(model: GridSortItem[]) => void` | — | Fired when the active sort model changes. |
49
50
  | `onFilterModelChange` | `(model: GridFilterModel) => void` | — | Fired when the active filter model changes. |
50
51
 
@@ -81,7 +82,6 @@ The main component for displaying and interacting with data.
81
82
  | `pinnedColumns` | `GridColumnPinning` | — | Columns pinned to the left or right viewport edges. See [Column & Row Pinning](#-column--row-pinning). |
82
83
  | `onPinnedColumnsChange` | `(model: GridColumnPinning) => void` | — | Fired when column pinning changes. |
83
84
  | `pinnedRows` | `GridRowPinning` | — | Row IDs pinned to the top or bottom of the viewport. |
84
- | `onPinnedRowsChange` | `(model: GridRowPinning) => void` | — | Fired when row pinning changes. |
85
85
  | `pinCheckboxColumn` | `boolean` | `false` | Keeps the checkbox column visible during horizontal scrolling. |
86
86
  | `pinExpandColumn` | `boolean` | `false` | Keeps the Master-Detail expansion column visible during horizontal scrolling. |
87
87
 
@@ -107,7 +107,7 @@ The main component for displaying and interacting with data.
107
107
  | :--- | :--- | :--- | :--- |
108
108
  | `treeData` | `boolean` | `false` | Enables hierarchical tree data display. |
109
109
  | `getTreeDataPath` | `(row: R) => string[]` | — | Returns the hierarchy path for a row (e.g. `['Engineering', 'Frontend']`). |
110
- | `groupingColDef` | `GridColDef` | — | Overrides the auto-generated tree expand/collapse column definition. |
110
+ | `groupingColDef` | `Partial<GridColDef>` | — | Configures the dedicated `__group__` column prepended at position 0 (auto-pinned left) when row grouping is active. Accepts `headerName`, `width`, `renderCell`, and any non-system `GridColDef` field. The column's `field` is always `'__group__'`; it is never sorted, exported, hidden, or re-ordered. |
111
111
  | `defaultGroupingExpansionDepth` | `number` | `0` | Number of tree levels expanded on initial render (`-1` = all). |
112
112
 
113
113
  #### Row Grouping & Aggregation
@@ -115,7 +115,6 @@ The main component for displaying and interacting with data.
115
115
  | Prop | Type | Default | Description |
116
116
  | :--- | :--- | :--- | :--- |
117
117
  | `rowGroupingModel` | `GridRowGroupingModel` | `[]` | Array of field names to group rows by (e.g. `['department', 'team']`). See [Row Grouping](#️-row-grouping). |
118
- | `onRowGroupingModelChange` | `(model: GridRowGroupingModel) => void` | — | Fired when the grouping model changes. |
119
118
  | `aggregationModel` | `GridAggregationModel` | — | Map of `field → aggFn` (e.g. `{ salary: 'sum', age: 'avg' }`). See [Aggregation Reference](#-aggregation-reference). |
120
119
  | `onAggregationModelChange` | `(model: GridAggregationModel) => void` | — | Fired when the aggregation model changes. |
121
120
  | `getAggregationPosition` | `(groupNode: GridTreeNode \| null) => 'inline' \| 'footer' \| null` | — | Controls where aggregation results appear. `'inline'` = inside the group row, `'footer'` = a dedicated row below the group, `null` = hidden. Pass `null` groupNode = grand-total (root) position. |
@@ -279,7 +278,7 @@ Defines the behavior and appearance of a single column.
279
278
  | :--- | :--- | :--- | :--- |
280
279
  | `field` | `string` | — | **Required.** Unique identifier matching the row object key. |
281
280
  | `headerName` | `string` | — | Text shown in the column header cell. |
282
- | `description` | `string` | — | Tooltip shown on header hover (accessibility label). |
281
+ | `description` | `string` | — | Tooltip shown on column header hover (rendered as the HTML `title` attribute — improves accessibility). |
283
282
  | `width` | `number \| string` | `100` | Fixed width in pixels or a percentage string. |
284
283
  | `minWidth` | `number` | — | Minimum width in pixels (enforced during resize). |
285
284
  | `maxWidth` | `number` | — | Maximum width in pixels (enforced during resize). |
@@ -335,9 +334,10 @@ Defines the behavior and appearance of a single column.
335
334
  | `pinnable` | `boolean` | `true` | Allow this column to be pinned via the UI. |
336
335
  | `disableColumnMenu` | `boolean` | `false` | Hide the column header kebab/context menu. |
337
336
  | `exportable` | `boolean` | `true` | Set to `false` to exclude from CSV, Excel, JSON, and Print exports. |
338
- | `groupable` | `boolean` | `true` | Allow this column to be used as a row grouping dimension. |
337
+ | `groupable` | `boolean` | `true` | Allow this column to be used as a row grouping dimension. Set to `false` to prevent this field from being grouped, even when it appears in `rowGroupingModel`. |
338
+ | `groupingValueFormatter` | `(params: { field: string; value: unknown }) => string` | — | Custom formatter for group-header labels when this column is the active grouping field. Falls back to `"field: value"` when omitted. |
339
339
  | `aggregable` | `boolean` | `true` | Allow this column to be aggregated. |
340
- | `availableAggregationFunctions` | `string[]` | all built-ins | Restrict which aggregation functions are available for this column (e.g. `['sum', 'avg']`). |
340
+ | `availableAggregationFunctions` | `string[]` | all built-ins | Restrict which aggregation functions are computed and available for this column (e.g. `['sum', 'avg']`). Functions outside this list are skipped even if set in `aggregationModel`. |
341
341
 
342
342
  ---
343
343
 
@@ -1087,7 +1087,6 @@ import { DataGrid, GridRowGroupingModel, GridAggregationModel } from '@opencores
1087
1087
  import { useState } from 'react';
1088
1088
 
1089
1089
  export default function GroupingExample() {
1090
- const [rowGroupingModel, setRowGroupingModel] = useState<GridRowGroupingModel>(['department']);
1091
1090
  const [aggregationModel, setAggregationModel] = useState<GridAggregationModel>({
1092
1091
  salary: 'sum',
1093
1092
  age: 'avg',
@@ -1097,8 +1096,7 @@ export default function GroupingExample() {
1097
1096
  <DataGrid
1098
1097
  rows={rows}
1099
1098
  columns={columns}
1100
- rowGroupingModel={rowGroupingModel}
1101
- onRowGroupingModelChange={setRowGroupingModel}
1099
+ rowGroupingModel={['department']}
1102
1100
  aggregationModel={aggregationModel}
1103
1101
  onAggregationModelChange={setAggregationModel}
1104
1102
  />
@@ -27,11 +27,29 @@ function MyGrid() {
27
27
  | `getRowId` | `(row: R) => GridRowId` | `row.id` | Unique ID for each row. |
28
28
  | `height` | `number \| string` | `undefined` | Height of the grid container. |
29
29
  | `loading` | `boolean` | `false` | Displays a loading skeleton/shimmer. |
30
+ | `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Row height preset: compact = 32 px, standard = `rowHeight`, comfortable = 72 px. |
30
31
  | `checkboxSelection` | `boolean` | `false` | Enable row checkboxes. |
32
+ | `disableRowSelectionOnClick` | `boolean` | `false` | When `true`, clicking a row does not toggle its selection. |
33
+ | `disableMultipleRowSelection` | `boolean` | `false` | When `true`, at most one row can be selected at a time. |
31
34
  | `pagination` | `boolean` | `false` | Enable/Disable bottom pagination bar. |
32
- | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). |
35
+ | `paginationModel` | `GridPaginationModel` | — | Controlled pagination state (`{ page, pageSize }`). Omit for uncontrolled; use `initialState` to set the initial page/pageSize. |
33
36
  | `pageSizeOptions` | `number[]` | `[10, 25, 50]` | Available page size options. |
34
- | `initialState` | `GridInitialState` | `undefined` | Initial configuration (sorting, columns, etc). |
37
+ | `initialState` | `GridInitialState` | `undefined` | Initial configuration (sorting, columns, pagination, etc). |
38
+
39
+ ## 🔃 Sorting
40
+
41
+ | Prop | Type | Default | Description |
42
+ | :--- | :--- | :--- | :--- |
43
+ | `sortModel` | `GridSortItem[]` | — | Controlled sort state. |
44
+ | `onSortModelChange` | `(model: GridSortItem[]) => void` | — | Fired when the sort model changes. |
45
+ | `multiSort` | `boolean` | `false` | When `true`, every click appends/cycles the column in the sort model instead of replacing it. Shift+click always appends regardless of this prop. |
46
+
47
+ ## 🌳 Row Grouping
48
+
49
+ | Prop | Type | Default | Description |
50
+ | :--- | :--- | :--- | :--- |
51
+ | `rowGroupingModel` | `string[]` | `[]` | Fields to group by (in order). |
52
+ | `groupingColDef` | `GridColDef` | — | Config for the dedicated `__group__` column created at position 0, auto-pinned left, when grouping is active. |
35
53
 
36
54
  ## 🖱️ Interaction & Events
37
55
 
@@ -9,7 +9,9 @@ Manages the column headers, sorting triggers, column resizing, and column groupi
9
9
  | `columns` | `GridColDef[]` | Column definitions for the visible viewport. |
10
10
  | `columnGroupingModel` | `GridColumnGroupingModel` | Hierarchy for multi-level header spanning. |
11
11
  | `sortModel` | `GridSortModel` | Current sorting state for highlight and icons. |
12
- | `onSort` | `(field, direction) => void` | Callback triggered when a header is clicked. |
12
+ | `onSort` | `(field, direction) => void` | Callback triggered on a plain click — replaces the sort model with a single key. |
13
+ | `onSortAdd` | `(field, direction) => void` | Callback triggered on Shift+click or when `multiSort` is active — appends/cycles the key without replacing others. |
14
+ | `multiSort` | `boolean` | When `true`, every click routes to `onSortAdd` instead of `onSort`. Wired from the `multiSort` prop on `<DataGrid>`. |
13
15
  | `onColumnResize` | `Function` | Callback for manual column width changes. |
14
16
  | `pinnedColumns` | `GridColumnPinning` | Coordinates sticky positioning for headers. |
15
17
  | `checkboxSelection` | `boolean` | Renders the "Select All" checkbox. |
@@ -30,23 +30,18 @@ The grid supports "Zebra" striping via CSS:
30
30
 
31
31
  When `getDetailPanelContent` is provided, the `<Row />` renders an expandable container below itself to show supplemental data.
32
32
 
33
- ## ⚠️ v1.1 — Hierarchy Field Deprecation
33
+ ## ⚠️ v2.0 — Runtime `_*` hierarchy fields removed
34
34
 
35
- The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer typed on `GridRowModel`. They will be removed from the runtime row object in v2.0.
36
-
37
- **Before (still works at runtime in v1.1 — `params.row._hasChildren` is `unknown`, not `boolean`; assignment to typed variables and arithmetic will error, truthiness checks will not):**
35
+ The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer injected onto the runtime row object as of v2.0. Use `params.rowMeta` instead:
38
36
 
39
37
  ```tsx
38
+ // ❌ v1 shim — no longer works in v2:
40
39
  renderCell: (params) => {
41
- // v1.1 accepts this at runtime; _hasChildren resolves to unknown via [key: string]: unknown
42
40
  const hasChildren = (params.row as Record<string, unknown>)._hasChildren;
43
41
  return hasChildren ? <GroupIcon /> : params.value;
44
42
  }
45
- ```
46
43
 
47
- **After (v1.1+):**
48
-
49
- ```tsx
44
+ // ✅ v2:
50
45
  renderCell: (params) => {
51
46
  const hasChildren = params.rowMeta?.hasChildren;
52
47
  return hasChildren ? <GroupIcon /> : params.value;
@@ -55,7 +55,6 @@ Rows can be pinned to the top or bottom of the grid. Pinned rows remain visible
55
55
  | `pinnedColumns` | `GridColumnPinning` | `{ left: string[], right: string[] }` |
56
56
  | `pinnedRows` | `GridRowPinning` | `{ top: GridRowId[], bottom: GridRowId[] }` |
57
57
  | `onPinnedColumnsChange` | `(model) => void` | Callback triggered when pinning changes. |
58
- | `onPinnedRowsChange` | `(model) => void` | Callback triggered when row pinning changes. |
59
58
 
60
59
  ---
61
60
 
@@ -36,6 +36,28 @@ const [selection, setSelection] = useState<GridRowId[]>([]);
36
36
  />
37
37
  ```
38
38
 
39
+ ### Disable Click-to-Select
40
+
41
+ Use `disableRowSelectionOnClick` to prevent clicking a row from changing the selection (useful when rows have their own click actions like navigation). Selection via checkboxes still works.
42
+
43
+ ```tsx
44
+ <DataGrid
45
+ checkboxSelection
46
+ disableRowSelectionOnClick
47
+ />
48
+ ```
49
+
50
+ ### Single-Row Selection Only
51
+
52
+ Use `disableMultipleRowSelection` to cap selection to one row at a time. Clicking a second row deselects the first; clicking an already-selected row deselects it.
53
+
54
+ ```tsx
55
+ <DataGrid
56
+ disableMultipleRowSelection
57
+ onRowSelectionModelChange={(model) => console.log('selected:', model)}
58
+ />
59
+ ```
60
+
39
61
  ---
40
62
 
41
63
  ## ⚙️ API Reference
@@ -15,6 +15,41 @@ Sorting can be toggled by clicking column headers. It supports single-column and
15
15
  | `sortModel` | `GridSortItem[]` | Controlled state of active sorts. |
16
16
  | `onSortModelChange` | `(model) => void` | Callback triggered when sorting changes. |
17
17
 
18
+ ### Multi-Column Sort
19
+
20
+ OpenGridX supports two modes of multi-column sorting:
21
+
22
+ **`multiSort` prop (single-click, recommended):** Pass `multiSort` to make every click append or cycle a column in the sort model rather than replacing it. No modifier key required.
23
+
24
+ ```tsx
25
+ <DataGrid
26
+ multiSort
27
+ sortModel={sortModel}
28
+ onSortModelChange={setSortModel}
29
+ ...
30
+ />
31
+ ```
32
+
33
+ **Shift+Click (always available):** Even without `multiSort`, holding **Shift** while clicking a column header appends it to the active sort. This works in all grids regardless of the prop.
34
+
35
+ Each active sort column shows a numbered priority badge (`1`, `2`, `3`…) next to its arrow.
36
+
37
+ - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc → desc → removed`.
38
+ - A plain click *without* `multiSort` and without Shift always resets to a single-key sort on that column.
39
+ - Set `sortable: false` on a column to exclude it entirely.
40
+
41
+ ```tsx
42
+ // Controlled multi-sort — set programmatically or drive from UI with multiSort:
43
+ <DataGrid
44
+ multiSort
45
+ sortModel={[
46
+ { field: 'department', sort: 'asc' },
47
+ { field: 'salary', sort: 'desc' },
48
+ ]}
49
+ onSortModelChange={setSortModel}
50
+ />
51
+ ```
52
+
18
53
  ### Column-Specific Config
19
54
  Disable sorting for specific columns in `GridColDef`:
20
55
  ```typescript
@@ -54,14 +54,57 @@ Tree data is used for data that has a natural parent-child relationship (e.g., a
54
54
  ---
55
55
 
56
56
  ## 🎨 Customizing the Group Column
57
- By default, OpenGridX creates a `__group__` column. You can customize its appearance:
57
+
58
+ When `rowGroupingModel` is active, pass `groupingColDef` to configure a dedicated `__group__` column that is prepended at position 0 and auto-pinned left, separate from your data columns. It accepts any `GridColDef` fields except `field` (always `'__group__'`).
58
59
 
59
60
  ```tsx
60
61
  <DataGrid
62
+ rows={rows}
63
+ columns={columns}
64
+ rowGroupingModel={['department']}
61
65
  groupingColDef={{
62
- headerName: 'Hierarchy',
63
- width: 300,
64
- renderCell: (params) => { /* custom renderer */ }
66
+ headerName: 'Department Group',
67
+ width: 240,
65
68
  }}
66
69
  />
67
70
  ```
71
+
72
+ Without `groupingColDef`, the group toggle is overlaid on the first data column. With it, the group indicator always appears in its own fixed column regardless of your `columns` order.
73
+
74
+ ---
75
+
76
+ ## 🏷️ Customizing Group Labels (`groupingValueFormatter`)
77
+
78
+ By default each group-header row displays `"field: value"`. Use `groupingValueFormatter` on a `GridColDef` to override the label for that grouping level:
79
+
80
+ ```tsx
81
+ const columns: GridColDef[] = [
82
+ {
83
+ field: 'department',
84
+ headerName: 'Department',
85
+ groupingValueFormatter: ({ value }) => `📁 ${String(value)}`,
86
+ },
87
+ { field: 'name', headerName: 'Name' },
88
+ { field: 'salary', headerName: 'Salary' },
89
+ ];
90
+
91
+ <DataGrid rows={rows} columns={columns} rowGroupingModel={['department']} />
92
+ // Group headers now show: "📁 Engineering", "📁 HR", etc.
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🚫 Preventing a Column from Being Grouped (`groupable: false`)
98
+
99
+ Set `groupable: false` on a `GridColDef` to prevent that field from being used as a grouping dimension. The column still renders normally, but the grid skips it when building the group tree:
100
+
101
+ ```tsx
102
+ const columns: GridColDef[] = [
103
+ { field: 'id', headerName: 'ID', groupable: false },
104
+ { field: 'department', headerName: 'Department' },
105
+ ];
106
+
107
+ // Even if rowGroupingModel includes 'id', it will be silently skipped.
108
+ <DataGrid rows={rows} columns={columns} rowGroupingModel={['id', 'department']} />
109
+ // Result: grouped by 'department' only — 'id' is skipped.
110
+ ```
@@ -27,7 +27,7 @@ import { GridListView } from './GridListView';
27
27
  import { GridPinnedRows } from './GridPinnedRows';
28
28
  import { GridVirtualRows } from './GridVirtualRows';
29
29
  import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
30
- import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem, GridRowMeta } from '../../types';
30
+ import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem, GridRowMeta, GridGroupedExportRow } from '../../types';
31
31
 
32
32
  const EMPTY_ROW_META_MAP: Map<GridRowId, GridRowMeta> = new Map();
33
33
 
@@ -39,7 +39,10 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
39
39
  rowHeight = 52,
40
40
  headerHeight = 56,
41
41
  autoHeight = false,
42
+ density,
42
43
  checkboxSelection = false,
44
+ disableRowSelectionOnClick = false,
45
+ disableMultipleRowSelection = false,
43
46
  rowSelectionModel: propRowSelectionModel,
44
47
  onRowSelectionModelChange: propOnRowSelectionModelChange,
45
48
  onRowClick,
@@ -50,7 +53,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
50
53
  sortModel: propSortModel,
51
54
  onSortModelChange,
52
55
  pagination: propPagination = false,
53
- paginationModel = { page: 0, pageSize: 100 },
56
+ paginationModel,
54
57
  onPaginationModelChange,
55
58
  pageSizeOptions = [10, 25, 50, 100],
56
59
  pinnedColumns: propPinnedColumns,
@@ -74,6 +77,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
74
77
  treeData = false,
75
78
  getTreeDataPath,
76
79
  defaultGroupingExpansionDepth,
80
+ groupingColDef,
77
81
 
78
82
  rowGroupingModel: propRowGroupingModel,
79
83
 
@@ -86,6 +90,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
86
90
  onProcessRowUpdateError,
87
91
 
88
92
  dataSource,
93
+ multiSort = false,
89
94
  sortingMode = 'client',
90
95
  filterMode = 'client',
91
96
  paginationMode = 'client',
@@ -191,8 +196,35 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
191
196
 
192
197
  const pivot = usePivot(rows as GridRowModel[], columns as unknown as GridColDef[], currentPivotModel, pivotMode);
193
198
 
199
+ const effectiveRowHeight = density === 'compact' ? 32 : density === 'comfortable' ? 72 : rowHeight;
200
+
194
201
  const activeRows = (pivotMode && pivot.isValid ? pivot.pivotRows : rows) as unknown as R[];
195
- const activeColumns = (pivotMode && pivot.isValid ? pivot.pivotColumns : columns) as unknown as GridColDef<R>[];
202
+ const baseColumns = (pivotMode && pivot.isValid ? pivot.pivotColumns : columns) as unknown as GridColDef<R>[];
203
+
204
+ // When groupingColDef is provided and row grouping is active, prepend a
205
+ // dedicated synthetic __group__ column at position 0 (pinned-left).
206
+ const isRowGroupingActive = Boolean(propRowGroupingModel && propRowGroupingModel.length > 0);
207
+
208
+ // Auto-pin __group__ column to the left when groupingColDef is active.
209
+ const effectivePinnedColumns = (groupingColDef && isRowGroupingActive && !pivotMode)
210
+ ? { ...pinnedColumns, left: ['__group__', ...((pinnedColumns?.left ?? []).filter(f => f !== '__group__'))] }
211
+ : pinnedColumns;
212
+
213
+ const activeColumns = (groupingColDef && isRowGroupingActive && !pivotMode)
214
+ ? [
215
+ {
216
+ headerName: 'Group',
217
+ width: 220,
218
+ ...groupingColDef,
219
+ field: '__group__',
220
+ hideable: false,
221
+ sortable: false,
222
+ pinnable: false,
223
+ exportable: false,
224
+ } as unknown as GridColDef<R>,
225
+ ...baseColumns,
226
+ ]
227
+ : baseColumns;
196
228
 
197
229
  const [serverAggregationResults, setServerAggregationResults] = useState<GridAggregationResult | null>(null);
198
230
 
@@ -245,7 +277,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
245
277
  const gridData = useDataGrid({
246
278
  rows: normalizedRows,
247
279
  columns: activeColumns,
248
- rowHeight,
280
+ rowHeight: effectiveRowHeight,
249
281
  headerHeight,
250
282
  rowCount: propRowCount,
251
283
  columnVisibilityModel,
@@ -309,6 +341,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
309
341
  const rowGroupingHandlers = useRowGrouping({
310
342
  rows: effectiveRows,
311
343
  getRowId: effectiveGetRowId,
344
+ columns: activeColumns,
312
345
  rowGroupingModel,
313
346
  aggregationModel,
314
347
  defaultGroupingExpansionDepth,
@@ -355,7 +388,27 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
355
388
  }
356
389
 
357
390
  onRowClick?.(params);
358
- }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap]);
391
+
392
+ if (!disableRowSelectionOnClick) {
393
+ let next: GridRowId[];
394
+ if (disableMultipleRowSelection) {
395
+ const alreadySelected = selectedRowIds.has(id as GridRowId);
396
+ next = alreadySelected ? [] : [id as GridRowId];
397
+ } else {
398
+ const newSelection = new Set(selectedRowIds);
399
+ if (newSelection.has(id as GridRowId)) {
400
+ newSelection.delete(id as GridRowId);
401
+ } else {
402
+ newSelection.add(id as GridRowId);
403
+ }
404
+ next = Array.from(newSelection);
405
+ }
406
+ if (!isSelectionControlled) {
407
+ setInternalRowSelectionModel(next);
408
+ }
409
+ onRowSelectionModelChange?.(next);
410
+ }
411
+ }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap, disableRowSelectionOnClick, disableMultipleRowSelection, selectedRowIds, isSelectionControlled, setInternalRowSelectionModel, onRowSelectionModelChange]);
359
412
 
360
413
  const dataSourceHandlers = useGridDataSource({
361
414
  dataSource,
@@ -440,7 +493,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
440
493
  columnWidths,
441
494
  effectiveColumnOrder,
442
495
  columnVisibilityModel,
443
- pinnedColumns,
496
+ pinnedColumns: effectivePinnedColumns,
444
497
  });
445
498
 
446
499
  const rowPipeline = useGridRowPipeline<GridRowModel>({
@@ -486,6 +539,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
486
539
 
487
540
  const { aggregationResult } = useAggregation({
488
541
  rows: filteredRows,
542
+ columns: activeColumns as unknown as GridColDef[],
489
543
  aggregationModel,
490
544
  isServerSide: !!(dataSource && (paginationMode === 'server' || sortingMode === 'server')),
491
545
  dataSource,
@@ -499,10 +553,56 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
499
553
  useEffect(() => {
500
554
  gridData.apiRef.current.getAggregationResult = () => hasAggregation ? aggregationResult : null;
501
555
  gridData.apiRef.current.getAggregationModel = () => hasAggregation ? aggregationModel : null;
502
- }, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef]);
556
+ gridData.apiRef.current.getGroupedExportRows = (): GridGroupedExportRow[] | null => {
557
+ if (!isRowGrouping) return null;
558
+ const { treeNodes, groupingRows } = rowGroupingHandlers;
559
+ const rowLookup = new Map<GridRowId, GridRowModel>();
560
+ sortedUnpinnedRows.forEach(r => rowLookup.set((r as GridRowModel).id as GridRowId, r as GridRowModel));
561
+
562
+ const result: GridGroupedExportRow[] = [];
563
+
564
+ const traverse = (ids: GridRowId[]) => {
565
+ ids.forEach(id => {
566
+ const node = treeNodes.get(id);
567
+ if (!node) return;
568
+ const isGroup = Boolean(node.children && node.children.length > 0);
569
+ if (isGroup) {
570
+ result.push({
571
+ type: 'group-header',
572
+ depth: node.depth,
573
+ groupField: node.groupingField,
574
+ groupValue: node.groupingValue,
575
+ });
576
+ traverse(node.children!);
577
+ if (hasAggregation && node.aggregatedValues) {
578
+ result.push({
579
+ type: 'group-subtotal',
580
+ depth: node.depth,
581
+ groupField: node.groupingField,
582
+ groupValue: node.groupingValue,
583
+ aggregatedValues: node.aggregatedValues,
584
+ });
585
+ }
586
+ } else {
587
+ const row = rowLookup.get(id) ?? groupingRows.get(id);
588
+ if (row) {
589
+ result.push({ type: 'leaf', depth: node.depth, row });
590
+ }
591
+ }
592
+ });
593
+ };
594
+
595
+ traverse(Array.from(treeNodes.keys()).filter(id => treeNodes.get(id)?.parentId === null));
596
+
597
+ if (hasAggregation && aggregationResult) {
598
+ result.push({ type: 'grand-total', depth: 0, aggregatedValues: aggregationResult });
599
+ }
600
+ return result;
601
+ };
602
+ }, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef, isRowGrouping, rowGroupingHandlers, sortedUnpinnedRows]);
503
603
 
504
604
  const layout = useLayout({
505
- rowHeight,
605
+ rowHeight: effectiveRowHeight,
506
606
  pagination,
507
607
  paginatedUnpinnedRows,
508
608
  sortedUnpinnedRows,
@@ -511,7 +611,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
511
611
  pinnedTopRowsLength: pinnedTopRows.length,
512
612
  pinnedBottomRowsLength: pinnedBottomRows.length,
513
613
  visibleOrderedColumns,
514
- pinnedColumns,
614
+ pinnedColumns: effectivePinnedColumns,
515
615
  columnWidths,
516
616
  viewportWidth: state.dimensions.viewportWidth || 1000,
517
617
  checkboxSelection,
@@ -642,6 +742,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
642
742
  onSortModelChange?.(newSortModel as GridSortItem[]);
643
743
  }, [isSortControlled, onSortModelChange, setInternalSortModel]);
644
744
 
745
+ const handleSortAdd = useCallback((field: string, direction: GridSortDirection) => {
746
+ const existing = sortModel.filter(item => item.field !== field);
747
+ const newSortModel: GridSortItem[] = direction
748
+ ? [...existing, { field, sort: direction }]
749
+ : existing;
750
+
751
+ if (!isSortControlled) {
752
+ setInternalSortModel(newSortModel);
753
+ }
754
+ onSortModelChange?.(newSortModel);
755
+ }, [sortModel, isSortControlled, onSortModelChange, setInternalSortModel]);
756
+
645
757
  const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
646
758
  allRenderableRows,
647
759
  navigationColumns,
@@ -763,7 +875,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
763
875
  style={{
764
876
  ...style,
765
877
  height: height ?? style?.height,
766
- '--ogx-row-height': `${rowHeight}px`,
878
+ '--ogx-row-height': `${effectiveRowHeight}px`,
767
879
  '--ogx-header-height': `${headerHeight}px`
768
880
  } as unknown as React.CSSProperties}
769
881
  aria-busy={effectiveLoading}
@@ -798,7 +910,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
798
910
  selectedRowIds={selectedRowIds}
799
911
  listViewColumn={listViewColumn}
800
912
  noRowsLabel={effectiveNoRowsLabel}
801
- rowHeight={rowHeight}
913
+ rowHeight={effectiveRowHeight}
802
914
  checkboxSelection={checkboxSelection}
803
915
  paginationMode={paginationMode}
804
916
  dataSource={dataSource}
@@ -863,10 +975,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
863
975
  onSelectAll={handleSelectAll}
864
976
  sortModel={sortModel}
865
977
  onSort={handleSort}
978
+ onSortAdd={handleSortAdd}
979
+ multiSort={multiSort}
866
980
 
867
981
  onColumnResize={handleColumnResize}
868
982
  columnWidths={columnWidths}
869
- pinnedColumns={pinnedColumns}
983
+ pinnedColumns={effectivePinnedColumns}
870
984
 
871
985
  focusedCell={focusedCell}
872
986
  onHeaderClick={(field) => {
@@ -923,7 +1037,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
923
1037
  onCellClick={handleCellClick}
924
1038
  onSelectionChange={handleSelectionChange}
925
1039
  columnWidths={columnWidths}
926
- pinnedColumns={pinnedColumns}
1040
+ pinnedColumns={effectivePinnedColumns}
927
1041
  pinnedRows={pinnedRows}
928
1042
  hasDetailPanel={hasDetailPanel}
929
1043
  expandedRowIds={expandedRowIds}
@@ -935,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
935
1049
  focusedCell={focusedCell}
936
1050
  colspanMap={spanning.colspanMap}
937
1051
  rowSpanningCaches={spanning.rowSpanningState.caches}
938
- rowHeight={rowHeight}
1052
+ rowHeight={effectiveRowHeight}
939
1053
  rowMetaMap={rowMetaMap}
940
1054
  />
941
1055
 
@@ -950,14 +1064,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
950
1064
  checkboxSelection={checkboxSelection}
951
1065
  hasDetailPanel={hasDetailPanel}
952
1066
  rowReordering={rowReordering}
953
- rowHeight={rowHeight}
1067
+ rowHeight={effectiveRowHeight}
954
1068
  pinnedRows={pinnedRows}
955
1069
  selectedRowIds={selectedRowIds}
956
1070
  onRowClick={handleRowClick}
957
1071
  onCellClick={handleCellClick}
958
1072
  onSelectionChange={handleSelectionChange}
959
1073
  columnWidths={columnWidths}
960
- pinnedColumns={pinnedColumns}
1074
+ pinnedColumns={effectivePinnedColumns}
961
1075
  expandedRowIds={expandedRowIds}
962
1076
  getDetailPanelContent={getDetailPanelContent}
963
1077
  getDetailPanelHeight={getDetailPanelHeight}
@@ -987,7 +1101,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
987
1101
  onCellClick={handleCellClick}
988
1102
  onSelectionChange={handleSelectionChange}
989
1103
  columnWidths={columnWidths}
990
- pinnedColumns={pinnedColumns}
1104
+ pinnedColumns={effectivePinnedColumns}
991
1105
  pinnedRows={pinnedRows}
992
1106
  hasDetailPanel={hasDetailPanel}
993
1107
  expandedRowIds={expandedRowIds}
@@ -999,7 +1113,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
999
1113
  focusedCell={focusedCell}
1000
1114
  colspanMap={spanning.colspanMap}
1001
1115
  rowSpanningCaches={spanning.rowSpanningState.caches}
1002
- rowHeight={rowHeight}
1116
+ rowHeight={effectiveRowHeight}
1003
1117
  rowMetaMap={rowMetaMap}
1004
1118
  />
1005
1119
 
@@ -1012,11 +1126,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1012
1126
  aggregationModel={aggregationModel}
1013
1127
  aggregationResult={aggregationResult}
1014
1128
  columnWidths={resolvedColumnWidths}
1015
- rowHeight={rowHeight}
1129
+ rowHeight={effectiveRowHeight}
1016
1130
  checkboxSelection={checkboxSelection}
1017
1131
  hasDetailPanel={hasDetailPanel}
1018
1132
  rowReordering={rowReordering}
1019
- pinnedColumns={pinnedColumns}
1133
+ pinnedColumns={effectivePinnedColumns}
1020
1134
  />
1021
1135
  )}
1022
1136