@opencorestack/opengridx 3.9.1 → 3.10.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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.d.ts +55 -0
  3. package/dist/opengridx.es.js +6346 -6279
  4. package/dist/opengridx.es.js.map +1 -1
  5. package/dist/opengridx.umd.js +16 -16
  6. package/dist/opengridx.umd.js.map +1 -1
  7. package/docs/API_REFERENCE.md +4 -0
  8. package/docs/features/ai-toolkit.md +1 -0
  9. package/docs/performance.md +1 -1
  10. package/docs/roadmap.md +5 -5
  11. package/lib/components/Cell/Cell.tsx +2 -1
  12. package/lib/components/DataGrid/DataGrid.tsx +19 -18
  13. package/lib/components/DataGrid/GridListView.tsx +1 -1
  14. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  15. package/lib/components/DataGrid/GridViewport.tsx +2 -1
  16. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  17. package/lib/components/Header/Header.tsx +4 -3
  18. package/lib/components/Row/Row.tsx +6 -2
  19. package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
  20. package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
  21. package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
  22. package/lib/components/Toolbar/GridToolbar.tsx +7 -380
  23. package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
  24. package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
  25. package/lib/hooks/core/gridKeyDown.ts +272 -0
  26. package/lib/hooks/core/useDataGrid.ts +13 -5
  27. package/lib/hooks/core/useGridApiMethods.ts +2 -2
  28. package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
  29. package/lib/hooks/core/useGridCellSelection.ts +17 -297
  30. package/lib/hooks/core/useGridColumnLookup.ts +2 -1
  31. package/lib/hooks/core/useGridColumns.tsx +3 -2
  32. package/lib/hooks/core/useGridControlledState.ts +3 -2
  33. package/lib/hooks/core/useGridHierarchy.ts +1 -1
  34. package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
  35. package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
  36. package/lib/hooks/core/useGridRowInteractions.ts +1 -1
  37. package/lib/hooks/core/useGridRowSelection.ts +11 -2
  38. package/lib/hooks/core/useGridToolbarProps.ts +3 -2
  39. package/lib/hooks/features/useGridAiAssistant.ts +3 -2
  40. package/lib/hooks/useColumnReorder.ts +2 -1
  41. package/lib/hooks/useTreeData.ts +4 -2
  42. package/lib/types/locale.ts +22 -0
  43. package/lib/utils/aggregation/index.ts +16 -0
  44. package/lib/utils/aiAssistant.ts +46 -15
  45. package/lib/utils/export/excelCells.ts +244 -0
  46. package/lib/utils/export/exportCsv.ts +179 -0
  47. package/lib/utils/export/exportExcelHtml.ts +220 -0
  48. package/lib/utils/export/exportJson.ts +179 -0
  49. package/lib/utils/export/exportRows.ts +85 -0
  50. package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
  51. package/lib/utils/export/index.ts +5 -695
  52. package/lib/utils/export/printGrid.ts +282 -0
  53. package/lib/utils/gridLocale.ts +30 -0
  54. package/lib/utils/selectionSets.ts +36 -0
  55. package/lib/utils/untypedColumns.ts +16 -0
  56. package/llms.txt +3 -1
  57. package/package.json +4 -2
@@ -988,6 +988,10 @@ interface GridLocaleText {
988
988
  aiAssistantAppliedChanges; aiAssistantRemoveChange(label); aiAssistantUndo; aiAssistantIgnored; aiAssistantIgnoredItem(message);
989
989
  aiAssistantEarlier; aiAssistantThinking; aiAssistantStopped; aiAssistantUndone; aiAssistantChangeRemoved;
990
990
  aiAssistantApplied(applied, ignored); aiAssistantNotValidated; aiAssistantFailed(message); aiAssistantUnknownError;
991
+ // AI assistant change chips (v3.10; filter operators in the chips use filterPanelOperators)
992
+ aiAssistantChipSearch(words); aiAssistantChipNoFilters; aiAssistantChipSort(field, 'asc' | 'desc'); aiAssistantChipNoSorting;
993
+ aiAssistantChipGroup(field); aiAssistantChipNoGrouping; aiAssistantChipSummary(fn, field); aiAssistantChipNoSummaries;
994
+ aiAssistantChipPivot(fields); aiAssistantChipNoPivot; aiAssistantChipVisibility(field, visible);
991
995
  // Pagination buttons
992
996
  paginationLabel; paginationFirstPageLabel; paginationFirstPageTooltip; paginationPreviousPageLabel;
993
997
  paginationPreviousPageTooltip; paginationNextPageLabel; paginationNextPageTooltip;
@@ -146,6 +146,7 @@ const onPrompt = createGridAiPromptHandler({
146
146
  - **Applying.** The reply is applied immediately as one undoable step. Each change shows as a removable chip: a filter item, the quick-filter words, a sort key, a grouping field, an aggregation, a column shown or hidden, the pivot. Removing a chip re-applies the reply without it (a part with no chip left returns to its value from before the reply). **Undo** restores the state from before the reply. This view-state undo is separate from the cell-edit undo history (`undoRedo`).
147
147
  - **What is applied.** Each part in `result.state` replaces the grid's model, except column visibility, which is merged over the current model (`{ tax: false }` hides one column and leaves the others). Parts outside `aiAssistant.parts` are ignored. A part the validator emptied (every entry dropped, such as a sort on an unknown field) is not applied, so it cannot clear the grid's model by accident. Changes go through the same handlers as the UI, so `onSortModelChange`, `onRowGroupingModelChange` and the rest fire; with a controlled model, update your state from them.
148
148
  - **What was dropped** is listed under the chips, such as `Ignored: Unknown field "revnue"`.
149
+ - **Translating.** Every string of the panel is a `localeText` key (v3.9), including the chips' own texts since v3.10: `aiAssistantChipSort: (field, direction) => \`Tri : ${field}, ${direction === 'desc' ? 'décroissant' : 'croissant'}\``, `aiAssistantChipSummary: (fn, field) => …` (`fn` is the function id, such as `avg`), and the other `aiAssistantChip*` keys in the API reference. Filter operators in the chips use `filterPanelOperators`.
149
150
  - **Accessibility.** The panel is a `role="dialog"` labelled "Ask AI". Focus moves to the input when it opens and back to the toolbar button (or wherever it was) when it closes. Escape closes it. The status ("Thinking…", "Applied 3 changes", errors) is announced through the grid's live region.
150
151
  - **Events.** `onAiAssistantApply(result)` after a reply was applied; `onAiAssistantError(error)` when `onPrompt` rejects (Stop does not count) or its result was not validated.
151
152
 
@@ -174,7 +174,7 @@ Measured in Chromium on an Apple M1, 1,000,000 rows, from the event to the end o
174
174
  | Fill 1,000 cells | 809 ms | ~50 ms | |
175
175
  | Expand a group | 279 ms | ~135 ms | leaf row metadata built once per tree |
176
176
  | Collapse a group | ~120 ms | ~1 ms | |
177
- | Select all | ~270 ms | ~270 ms | bounded by building 1M-id sets for the array `rowSelectionModel` |
177
+ | Select all | ~270 ms | ~270 ms (~130 ms in 3.10) | 3.10 builds the 1M-id set once and reuses it for the model it returns; the rest is the live-selection check |
178
178
 
179
179
  3.9.0 keeps rows memoised: selecting a row, starting or committing an edit, and vertical scrolling re-render only the rows they change (a click re-rendered every visible row, now one). React's own work per interaction is about 3–5 ms at 100k rows.
180
180
 
package/docs/roadmap.md CHANGED
@@ -93,16 +93,16 @@ This document tracks the current status of features in `OpenGridX` and outlines
93
93
  | **3.8.1** ✅ | shipped 13 Oct | **Grid buttons keep their own styles** (AI panel, toolbar active states) | Exact `bigint` sort; stricter number text; dead-code cleanup; every export documented |
94
94
  | **3.9.0** ✅ | shipped 13 Oct | **`localeText` for every UI string** (~200 keys) | 9 theme keys + Theme Builder controls; rows stay memoised through selection and editing; demo sidebar by priority |
95
95
  | **3.9.1** ✅ | shipped 13 Oct | **npm package no longer ships 1.6 MB of test baselines** | `check:package` guard in CI; version badges read `package.json`; docs brought up to date |
96
- | 3.10.0 | open | AI change chips translatable | Backlog work below |
96
+ | **3.10.0** ✅ | shipped 13 Oct | **Backlog cleared**: refactors (R4–R6), CI guards, select-all ~2× faster | AI change chips translatable; demo syntax highlighting |
97
97
  | Freeze | 19 Dec – 5 Jan | Bug fixes only | — |
98
98
 
99
- ### Now: backlog first (feature hold since 13 Oct)
99
+ ### Now: backlog first (feature hold since 13 Oct; backlog cleared in 3.10)
100
100
 
101
101
  New features (the *Next* list below) wait until the open fixes, refactors and performance work are done. Tracked in [`superpowers/plans/2026-10-13-audit-tracker.md`](superpowers/plans/2026-10-13-audit-tracker.md):
102
102
 
103
- * **Refactors** (no behaviour change): about 40 `as unknown as` casts (R4), files over 600 lines such as `GridToolbar.tsx` (R5), hooks with very wide parameter lists such as keyboard navigation (R6).
104
- * **Performance**: the time-sliced filter / sort pipeline (held: 1.5–2× slower in total, needs to be within ~20 %); select-all at 1M rows (~270 ms, bounded by the array `rowSelectionModel`).
105
- * **Demo**: syntax highlighting in the source viewer.
103
+ * ✅ **Refactors** (no behaviour change, 3.10): `as unknown as` casts 43 → 15 (R4), files over 600 lines split (R5), keydown handling as a typed, tested function (R6); CI guards against regressions (`check:compiler`, `check:versions`, ESLint `no-explicit-any` and no new `as unknown as`).
104
+ * **Performance**: ✅ select-all at 1M rows ~260 → ~130 ms (3.10, no API change). ❌ The time-sliced filter / sort pipeline is closed: it was 1.5–2× slower in total (target ≤ 20 %) and the remaining 1M-row sort pause is better served by a `dataSource`; see the [proposal's status](superpowers/specs/2026-10-12-nonblocking-pipeline-proposal.md).
105
+ * ✅ **Demo**: syntax highlighting in the source viewer (3.10).
106
106
  * **Bundle size (4.0)**: an app using only `DataGrid` ships 87 KB gzipped; opt-in [feature modules](superpowers/specs/2026-10-13-feature-modules-proposal.md) would save ~20 KB. Breaking, so planned for 4.0.
107
107
 
108
108
  ### Across the quarter
@@ -8,6 +8,7 @@ import { CellErrorBoundary } from './CellErrorBoundary';
8
8
  import { formatValueByType } from '../../utils/values';
9
9
  import { attempt } from '../../utils/attempt';
10
10
  import { RANGE_BOTTOM, RANGE_CELL, RANGE_FILL_HANDLE, RANGE_LEFT, RANGE_RIGHT, RANGE_TOP } from '../../utils/cellSelection';
11
+ import { untypedColumn } from '../../utils/untypedColumns';
11
12
 
12
13
  export interface CellProps<R extends GridRowModel = GridRowModel> {
13
14
  onCellClick?: (params: GridCellParams<R>) => void;
@@ -306,7 +307,7 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
306
307
  formattedValue={formattedValue}
307
308
  row={row}
308
309
  field={colDef.field}
309
- colDef={colDef as unknown as GridColDef}
310
+ colDef={untypedColumn(colDef)}
310
311
  rowIndex={rowIndex}
311
312
  colIndex={colIndex}
312
313
  rowMeta={rowMeta}
@@ -3,7 +3,7 @@ import { useLayout } from '../../hooks/core/useLayout';
3
3
  import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavigation';
4
4
  import { useGridControlledState } from '../../hooks/core/useGridControlledState';
5
5
  import { useGridThemeDimensions } from '../../hooks/core/useGridThemeDimensions';
6
- import { useGridRowPipeline, type GridRowPipelineResult } from '../../hooks/core/useGridRowPipeline';
6
+ import { useGridRowPipeline } from '../../hooks/core/useGridRowPipeline';
7
7
  import { countReachableRows } from '../../utils/maxElementHeight';
8
8
  import { useGridColumns } from '../../hooks/core/useGridColumns';
9
9
  import { useGridDetailPanel } from '../../hooks/core/useGridDetailPanel';
@@ -44,7 +44,8 @@ import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/co
44
44
  import { getPaginationRowCount } from '../../utils/pagination';
45
45
  import { useGridToolbarStack } from '../../hooks/core/useGridToolbarStack';
46
46
  import { DEFAULT_HEADER_FILTER_HEIGHT } from '../../utils/headerFilters';
47
- import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridRowId, GridColDef } from '../../types';
47
+ import type { DataGridProps, DataGridUntypedColumnsProps, GridValidRowModel, GridRowModel, GridColDef } from '../../types';
48
+ import { untypedColumns } from '../../utils/untypedColumns';
48
49
 
49
50
  /**
50
51
  * The grid. `R` is your row type: any object type (an interface works), inferred from `rows`. `columns`
@@ -133,7 +134,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
133
134
 
134
135
  const pivot = useGridPivot({
135
136
  rows: rows as GridRowModel[],
136
- columns: columns as unknown as GridColDef[],
137
+ columns: untypedColumns(columns),
137
138
  pivotMode,
138
139
  pivotModel: currentPivotModel,
139
140
  filterModel,
@@ -166,7 +167,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
166
167
  });
167
168
 
168
169
  const activeRows = pivot.rows as unknown as R[];
169
- const baseColumns = pivot.columns as unknown as GridColDef<R>[];
170
+ const baseColumns = pivot.columns as GridColDef<R>[];
170
171
 
171
172
  const { activeColumns, effectivePinnedColumns } = useGridGroupingColumn<R>({
172
173
  baseColumns, pinnedColumns, hasGroupingColumn, groupingColDef, isTreeData, getTreeDataPath,
@@ -222,13 +223,13 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
222
223
 
223
224
  // Steps 12a–13: the edit commit channel, the batch edit and single-cell editing.
224
225
  const { editCommitChannel, batchEdit, editingHandlers } = useGridEditPipeline<R>({
225
- rowsLookup: state.rows.idRowsLookup as unknown as ReadonlyMap<GridRowId, R>,
226
+ rowsLookup: gridData.rowsLookup,
226
227
  rows: effectiveRows,
227
228
  getRowId: getRowIdOf,
228
229
  columns: activeColumns,
229
230
  processRowUpdate,
230
231
  onProcessRowUpdateError,
231
- replaceRows: replaceRows as unknown as (rows: ReadonlyMap<GridRowId, R>) => void,
232
+ replaceRows,
232
233
  });
233
234
 
234
235
  // The selection without ids of removed rows. Only client-owned rows are pruned: a server page
@@ -241,12 +242,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
241
242
  onSelectionModelChange: handleRowSelectionModelChange,
242
243
  });
243
244
 
244
- const dataSourceHandlers = useGridDataSource({
245
+ const dataSourceHandlers = useGridDataSource<R>({
245
246
  dataSource, sortModel, filterModel,
246
247
  paginationModel: effectivePaginationModel,
247
248
  paginationMode, sortingMode, filterMode, aggregationModel,
248
249
  rowGroupingActive: isRowGrouping,
249
- getRowId: effectiveGetRowId as unknown as (row: GridRowModel) => GridRowId,
250
+ getRowId: effectiveGetRowId,
250
251
  setRows, setRowCount, setDataSourceLoading, setDataSourceError,
251
252
  onAggregationResults: setServerAggregationResults,
252
253
  onPaginationModelChange: handlePaginationModelChange,
@@ -290,8 +291,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
290
291
  density: effectiveDensity,
291
292
  });
292
293
 
293
- const rowPipeline = useGridRowPipeline<GridRowModel>({
294
- effectiveRows: effectiveRows as GridRowModel[],
294
+ const rowPipeline = useGridRowPipeline<R>({
295
+ effectiveRows,
295
296
  activeHierarchyHandlers,
296
297
  filterMode,
297
298
  filterModel: pivot.pipelineFilterModel,
@@ -303,7 +304,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
303
304
  });
304
305
  const {
305
306
  filteredRows, dataRows, pinnedTopRows, pinnedBottomRows, sortedUnpinnedRows, paginatedUnpinnedRows, allRenderableRows,
306
- } = rowPipeline as unknown as GridRowPipelineResult<R>;
307
+ } = rowPipeline;
307
308
  // The unpinned rows on screen: the current page, or every row without pagination.
308
309
  const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
309
310
 
@@ -345,7 +346,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
345
346
  hierarchyField,
346
347
  dataRows,
347
348
  }),
348
- visibleColumns: renderOrderedColumns as unknown as GridColDef[],
349
+ visibleColumns: renderOrderedColumns,
349
350
  },
350
351
  });
351
352
 
@@ -359,7 +360,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
359
360
 
360
361
  const { aggregationResult, isLoading: isAggregationLoading } = useAggregation({
361
362
  rows: dataRows,
362
- columns: activeColumns as unknown as GridColDef[],
363
+ columns: untypedColumns(activeColumns),
363
364
  // The pivot's Grand Total row is the aggregate in pivot mode: nothing to total here.
364
365
  aggregationModel: isPivotActive ? NO_AGGREGATION_MODEL : aggregationModel,
365
366
  // When the server drives the rows (paginates, sorts or filters them), the grid only holds
@@ -437,8 +438,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
437
438
  // Steps 30–31a: copy, scrollToIndexes and column auto-size on the API.
438
439
  const handleColumnAutosize = useGridViewApis<R>({
439
440
  apiRef, containerRef, viewportRef,
440
- copyColumns: renderedDataColumns as unknown as GridColDef[],
441
- getRowId: getRowIdOf as unknown as (row: GridRowModel) => GridRowId,
441
+ copyColumns: renderedDataColumns,
442
+ getRowId: getRowIdOf,
442
443
  disableClipboardCopy,
443
444
  getCellRangeCopyText: cellSelection.enabled ? cellSelection.getCopyText : undefined,
444
445
  layout,
@@ -497,7 +498,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
497
498
  getPendingModel: controlledState.getPendingCellSelectionModel,
498
499
  focusedCell, setFocusedCell, allRenderableRows,
499
500
  getRowId: getRowIdOf,
500
- navigationColumns: navigationColumns as unknown as GridColDef<R>[],
501
+ navigationColumns,
501
502
  columnIndexMap, isCellEditable, rowMetaMap,
502
503
  getSpanOrigin: spanning.getSpanOrigin,
503
504
  isEditing: editingHandlers.editingCell !== null,
@@ -559,7 +560,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
559
560
  apiRef, containerRef, localeText,
560
561
  aiAssistant: {
561
562
  options: aiAssistant,
562
- columns: activeColumns as unknown as GridColDef[],
563
+ columns: untypedColumns(activeColumns),
563
564
  onApply: onAiAssistantApply,
564
565
  onError: onAiAssistantError,
565
566
  },
@@ -598,7 +599,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
598
599
  pinnedTopRows, pinnedBottomRows, pinnedRows, getRowIdOf,
599
600
  showEmptyState: !effectiveLoading && !state.dataSource.error && filteredRows.length === 0,
600
601
  effectiveNoRowsLabel, spanRowWindow, visibleRows,
601
- baseColumns: columns as unknown as GridColDef<R>[], viewportWidth: state.dimensions.viewportWidth,
602
+ baseColumns: columns as GridColDef<R>[], viewportWidth: state.dimensions.viewportWidth,
602
603
  rowReorderHandlers, paginationMode, dataSourceLoading: state.dataSource.loading,
603
604
  sortedUnpinnedRowCount: sortedUnpinnedRows.length, pageSize: effectivePaginationModel.pageSize,
604
605
  unpinnedRowsLength: layout.unpinnedRowsLength,
@@ -35,7 +35,7 @@ export interface GridListViewProps<R extends GridRowModel> {
35
35
  /** The page actually shown (the requested page clamped to the last page). */
36
36
  currentPage: number;
37
37
  pageSizeOptions: number[];
38
- selectedRowIds: Set<GridRowId>;
38
+ selectedRowIds: ReadonlySet<GridRowId>;
39
39
  listViewColumn: GridListViewColDef<R>;
40
40
  /** Grid columns by field; a list field that names one gets its valueGetter / valueFormatter. */
41
41
  columnsByField: ReadonlyMap<string, GridColDef>;
@@ -31,7 +31,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
31
31
  rows: R[];
32
32
  position: 'top' | 'bottom';
33
33
  columns: GridColDef<R>[];
34
- selectedRowIds: Set<GridRowId>;
34
+ selectedRowIds: ReadonlySet<GridRowId>;
35
35
  checkboxSelection: boolean;
36
36
  onRowClick: (params: GridRowParams<R>) => void;
37
37
  onRowDoubleClick?: (params: GridRowParams<R>) => void;
@@ -15,6 +15,7 @@ import type { UseGridViewportResult } from '../../hooks/core/useGridViewport';
15
15
  import type { GridVirtualizationResult } from '../../hooks/core/useGridVirtualization';
16
16
  import type { UseColumnReorderReturn } from '../../hooks/useColumnReorder';
17
17
  import type { GridSpanningResult } from '../../hooks/features/useGridSpanning';
18
+ import { untypedColumns } from '../../utils/untypedColumns';
18
19
 
19
20
  /** The props the row renderers (top-pinned, centre and bottom-pinned rows) all take. */
20
21
  export type GridRowRenderProps<R extends GridRowModel> = Pick<GridVirtualRowsProps<R>,
@@ -294,7 +295,7 @@ export function GridViewport<R extends GridRowModel>(props: GridViewportProps<R>
294
295
  hidden, pinned and scrolled columns line up. Off in pivot mode (hasAggregation). */}
295
296
  {showAggregationFooter && (
296
297
  <GridAggregationFooter
297
- columns={virtualization.virtualColumns as unknown as GridColDef[]}
298
+ columns={untypedColumns(virtualization.virtualColumns)}
298
299
  aggregationModel={aggregationModel}
299
300
  aggregationResult={aggregationResult}
300
301
  columnWidths={columnWidths}
@@ -41,7 +41,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
41
41
  hasDetailPanel: boolean;
42
42
  rowReordering: boolean;
43
43
  rowHeight: number;
44
- selectedRowIds: Set<GridRowId>;
44
+ selectedRowIds: ReadonlySet<GridRowId>;
45
45
  onRowClick: (params: GridRowParams<R>) => void;
46
46
  onRowDoubleClick?: (params: GridRowParams<R>) => void;
47
47
  onCellClick: (params: GridCellParams<R>) => void;
@@ -12,6 +12,7 @@ import { getHeaderColumnStyle } from './headerColumnStyle';
12
12
  import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
13
13
  import { isRtl, logicalArrowKey } from '../../utils/direction';
14
14
  import { useGridLocale } from '../DataGrid/gridLocaleContext';
15
+ import { untypedColumn, untypedColumns } from '../../utils/untypedColumns';
15
16
 
16
17
 
17
18
  function MenuIcon() {
@@ -469,7 +470,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
469
470
  };
470
471
 
471
472
  const headerContent = colDef.renderHeader
472
- ? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex })
473
+ ? colDef.renderHeader({ field: colDef.field, colDef: untypedColumn(colDef), colIndex })
473
474
  : colDef.headerName || colDef.field;
474
475
 
475
476
  const handleHeaderClick = (e: React.MouseEvent) => {
@@ -560,7 +561,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
560
561
 
561
562
  {menuOpenParams && (
562
563
  <ColumnMenu
563
- colDef={menuOpenParams.colDef as unknown as GridColDef<GridRowModel>}
564
+ colDef={untypedColumn(menuOpenParams.colDef)}
564
565
  sortModel={sortModel}
565
566
  onSort={onSort || onSortAdd ? handleMenuSort : undefined}
566
567
  onHide={onHideColumn}
@@ -576,7 +577,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
576
577
  {/* ── Header filter row ── */}
577
578
  {headerFilters && (
578
579
  <HeaderFilterRow
579
- columns={columns as unknown as GridColDef[]}
580
+ columns={untypedColumns(columns)}
580
581
  columnWidths={columnWidths}
581
582
  pinnedColumns={pinnedColumns}
582
583
  pinnedPositions={pinnedPositions}
@@ -108,6 +108,10 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
108
108
  cellRange?: GridRowCellRange | null;
109
109
  }
110
110
 
111
+ // The system columns' definitions passed to error params of their cells (stable, module level).
112
+ const EXPAND_COLUMN: GridColDef = { field: '__expand_col__', width: 48 };
113
+ const CHECKBOX_COLUMN: GridColDef = { field: '__checkbox_col__', width: 48 };
114
+
111
115
  function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
112
116
  const locale = useGridLocale();
113
117
  const {
@@ -341,7 +345,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
341
345
  row,
342
346
  field: '__expand_col__',
343
347
  value: isDetailPanelExpanded,
344
- colDef: { field: '__expand_col__', width: 48 } as unknown as GridColDef<R>,
348
+ colDef: EXPAND_COLUMN as GridColDef<R>,
345
349
  rowIndex,
346
350
  colIndex: -1
347
351
  });
@@ -379,7 +383,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
379
383
  row,
380
384
  field: '__checkbox_col__',
381
385
  value: isSelected,
382
- colDef: { field: '__checkbox_col__', width: 48 } as unknown as GridColDef<R>,
386
+ colDef: CHECKBOX_COLUMN as GridColDef<R>,
383
387
  rowIndex,
384
388
  colIndex: -1
385
389
  });
@@ -0,0 +1,90 @@
1
+ import ReactDOM from 'react-dom';
2
+ import type { GridAggregationModel, GridColDef } from '../../types';
3
+ import { getOfferedAggregationFunctions } from '../../utils/aggregation';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
5
+ import { SigmaIcon } from './ToolbarIcons';
6
+ import { useToolbarPanel } from './useToolbarPanel';
7
+
8
+ /** The toolbar's Summaries panel: one row of function pills per aggregable column. */
9
+ export function AggregationPanel({
10
+ anchorRef,
11
+ aggregationModel,
12
+ aggregableColumns,
13
+ onFunctionChange,
14
+ onClearAll,
15
+ onClose,
16
+ activeCount,
17
+ }: {
18
+ anchorRef: React.RefObject<HTMLElement | null>;
19
+ aggregationModel: GridAggregationModel;
20
+ aggregableColumns: GridColDef[];
21
+ onFunctionChange: (field: string, fn: string) => void;
22
+ onClearAll: () => void;
23
+ onClose: () => void;
24
+ activeCount: number;
25
+ }) {
26
+ const locale = useGridLocale();
27
+ const { panelRef, position: pos, portalContainer } = useToolbarPanel(anchorRef, onClose);
28
+
29
+ const panel = (
30
+ <div
31
+ ref={panelRef}
32
+ className="ogx-toolbar__panel"
33
+ role="dialog"
34
+ aria-label={locale.toolbarSummariesPanelLabel}
35
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 400, maxWidth: 'calc(100vw - 24px)' }}
36
+ >
37
+ <div className="ogx-toolbar__panel-header">
38
+ <span className="ogx-toolbar__panel-title">
39
+ <SigmaIcon />
40
+ {locale.toolbarSummaries}
41
+ </span>
42
+ {activeCount > 0 && (
43
+ <button
44
+ type="button"
45
+ className="ogx-toolbar__clear-btn"
46
+ onClick={onClearAll}
47
+ title={locale.toolbarSummariesClearAllTooltip}
48
+ >
49
+ {locale.toolbarSummariesClearAll}
50
+ </button>
51
+ )}
52
+ </div>
53
+
54
+ <div className="ogx-toolbar__panel-body">
55
+ {aggregableColumns.length === 0 ? (
56
+ <p className="ogx-toolbar__empty">
57
+ {locale.toolbarSummariesEmpty}<br />
58
+ {`${locale.toolbarSummariesEmptyHintBefore} `}<code>aggregable: true</code>{` ${locale.toolbarSummariesEmptyHintAfter}`}
59
+ </p>
60
+ ) : (
61
+ aggregableColumns.map((col) => {
62
+ const currentFn = aggregationModel[col.field] || 'none';
63
+ return (
64
+ <div key={col.field} className="ogx-toolbar__agg-row">
65
+ <span className="ogx-toolbar__agg-label" title={col.field}>
66
+ {col.headerName || col.field}
67
+ </span>
68
+ <div className="ogx-toolbar__agg-pills">
69
+ {['none', ...getOfferedAggregationFunctions(col)].map((fn) => (
70
+ <button
71
+ type="button"
72
+ key={fn}
73
+ className={`ogx-toolbar__pill${currentFn === fn ? ' ogx-toolbar__pill--active' : ''}`}
74
+ onClick={() => onFunctionChange(col.field, fn)}
75
+ aria-pressed={currentFn === fn}
76
+ >
77
+ {locale.toolbarAggregationFunction(fn)}
78
+ </button>
79
+ ))}
80
+ </div>
81
+ </div>
82
+ );
83
+ })
84
+ )}
85
+ </div>
86
+ </div>
87
+ );
88
+
89
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
90
+ }
@@ -0,0 +1,56 @@
1
+ import ReactDOM from 'react-dom';
2
+ import type { GridColDef } from '../../types';
3
+ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
5
+ import { useToolbarPanel } from './useToolbarPanel';
6
+
7
+ /** The toolbar's Columns panel: `ColumnVisibilityPanel` in a positioned, portalled popover. */
8
+ export function ColumnsPanelWrapper({
9
+ anchorRef,
10
+ columns,
11
+ visibleColumns,
12
+ onVisibilityChange,
13
+ onShowAll,
14
+ onHideAll,
15
+ onColumnReorder,
16
+ onColumnOrderReset,
17
+ onClose,
18
+ showNonHideableColumns,
19
+ }: {
20
+ anchorRef: React.RefObject<HTMLElement | null>;
21
+ columns: GridColDef[];
22
+ visibleColumns: Set<string>;
23
+ onVisibilityChange: (field: string, isVisible: boolean) => void;
24
+ onShowAll: () => void;
25
+ onHideAll: () => void;
26
+ onColumnReorder?: (fromField: string, toField: string) => void;
27
+ onColumnOrderReset?: () => void;
28
+ onClose: () => void;
29
+ showNonHideableColumns?: boolean;
30
+ }) {
31
+ const locale = useGridLocale();
32
+ const { panelRef, position: pos, portalContainer } = useToolbarPanel(anchorRef, onClose);
33
+
34
+ const panel = (
35
+ <div
36
+ ref={panelRef}
37
+ className="ogx-toolbar__panel"
38
+ role="dialog"
39
+ aria-label={locale.toolbarColumnsPanelLabel}
40
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 280, padding: 0 }}
41
+ >
42
+ <ColumnVisibilityPanel
43
+ columns={columns}
44
+ visibleColumns={visibleColumns}
45
+ onVisibilityChange={onVisibilityChange}
46
+ onShowAll={onShowAll}
47
+ onHideAll={onHideAll}
48
+ onColumnReorder={onColumnReorder}
49
+ onColumnOrderReset={onColumnOrderReset}
50
+ showNonHideableColumns={showNonHideableColumns}
51
+ />
52
+ </div>
53
+ );
54
+
55
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
56
+ }
@@ -0,0 +1,82 @@
1
+ import ReactDOM from 'react-dom';
2
+ import type { GridColDef, GridFilterModel } from '../../types';
3
+ import { FilterPanel } from '../FilterPanel/FilterPanel';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
5
+ import { FilterIcon } from './ToolbarIcons';
6
+ import { useToolbarPanel } from './useToolbarPanel';
7
+
8
+ /** The toolbar's Filters panel: `FilterPanel` with a count, Clear all and Close. */
9
+ export function FilterPanelWrapper({
10
+ anchorRef,
11
+ columns,
12
+ filterModel,
13
+ onFilterModelChange,
14
+ onClose
15
+ }: {
16
+ anchorRef: React.RefObject<HTMLElement | null>;
17
+ columns: GridColDef[];
18
+ filterModel: GridFilterModel;
19
+ onFilterModelChange: (model: GridFilterModel) => void;
20
+ onClose: () => void;
21
+ }) {
22
+ const locale = useGridLocale();
23
+ // Escape closes it; a press outside does not: users type in its inputs, and it has a Close button.
24
+ const { panelRef, position: pos, portalContainer } = useToolbarPanel(anchorRef, onClose, { closeOnOutsideClick: false });
25
+
26
+ const activeFilterCount = filterModel.items?.length ?? 0;
27
+
28
+ const handleClearAll = () => {
29
+ onFilterModelChange({ ...filterModel, items: [], logicOperator: 'and' });
30
+ };
31
+
32
+ const panel = (
33
+ <div
34
+ ref={panelRef}
35
+ className="ogx-toolbar__panel ogx-toolbar__panel--filter"
36
+ role="dialog"
37
+ aria-label={locale.toolbarFiltersLabel}
38
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
39
+ >
40
+ <div className="ogx-toolbar__panel-header">
41
+ <span className="ogx-toolbar__panel-title">
42
+ <FilterIcon />
43
+ {locale.toolbarFilters}
44
+ {activeFilterCount > 0 && (
45
+ <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
46
+ )}
47
+ </span>
48
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
49
+ {activeFilterCount > 0 && (
50
+ <button
51
+ type="button"
52
+ className="ogx-toolbar__clear-btn"
53
+ onClick={handleClearAll}
54
+ title={locale.toolbarFiltersClearAllTooltip}
55
+ >
56
+ {locale.toolbarFiltersClearAll}
57
+ </button>
58
+ )}
59
+ <button
60
+ type="button"
61
+ className="ogx-toolbar__close-btn"
62
+ onClick={onClose}
63
+ title={locale.toolbarFiltersCloseTooltip}
64
+ aria-label={locale.toolbarFiltersCloseLabel}
65
+ >
66
+ {locale.toolbarFiltersClose}
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="ogx-toolbar__panel-body ogx-toolbar__panel-body--filter">
72
+ <FilterPanel
73
+ columns={columns}
74
+ filterModel={filterModel}
75
+ onFilterModelChange={onFilterModelChange}
76
+ />
77
+ </div>
78
+ </div>
79
+ );
80
+
81
+ return portalContainer ? ReactDOM.createPortal(panel, portalContainer) : null;
82
+ }