@opencorestack/opengridx 0.1.8 → 1.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.
- package/CHANGELOG.md +59 -0
- package/dist/index.d.ts +60 -41
- package/dist/opengridx.es.js +5271 -4660
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/README.md +14 -0
- package/docs/components/aggregation-footer.md +48 -0
- package/docs/components/datagrid.md +3 -0
- package/docs/components/empty-state.md +50 -0
- package/docs/components/error-overlay.md +40 -0
- package/docs/roadmap.md +20 -1
- package/lib/components/Cell/Cell.tsx +61 -64
- package/lib/components/Cell/GridEditInputCell.tsx +4 -4
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +58 -0
- package/lib/components/DataGrid/DataGrid.tsx +396 -1654
- package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
- package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
- package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
- package/lib/components/DataGrid/GridListView.tsx +116 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
- package/lib/components/Header/ColumnMenu.tsx +2 -2
- package/lib/components/Header/Header.tsx +4 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Row/Row.tsx +46 -45
- package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
- package/lib/components/Toolbar/GlobalSearch.tsx +1 -1
- package/lib/components/Toolbar/GridToolbar.tsx +26 -5
- package/lib/components/Toolbar/PivotPanel.tsx +7 -2
- package/lib/components/ui/DragHandleIcon.tsx +1 -1
- package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
- package/lib/hooks/core/useDataGrid.ts +15 -35
- package/lib/hooks/core/useGridColumns.tsx +228 -0
- package/lib/hooks/core/useGridControlledState.ts +192 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +371 -0
- package/lib/hooks/core/useGridRowPipeline.ts +109 -0
- package/lib/hooks/core/useGridScrollSync.ts +52 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
- package/lib/hooks/core/useGridVirtualization.ts +186 -0
- package/lib/hooks/core/useGridVisibleRows.ts +66 -0
- package/lib/hooks/core/useLayout.ts +348 -0
- package/lib/hooks/features/useAggregation.test.ts +135 -0
- package/lib/hooks/features/useAggregation.ts +7 -7
- package/lib/hooks/features/useGridClipboard.ts +1 -1
- package/lib/hooks/features/useGridDataSource.ts +2 -2
- package/lib/hooks/features/useGridEditing.ts +31 -32
- package/lib/hooks/features/usePivot.test.ts +202 -0
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/useColumnReorder.ts +1 -1
- package/lib/hooks/useRowGrouping.ts +14 -14
- package/lib/hooks/useTreeData.ts +1 -1
- package/lib/state/types.ts +1 -1
- package/lib/state/useGridStateStorage.ts +2 -2
- package/lib/types/index.ts +48 -32
- package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
- package/lib/utils/export/index.ts +23 -18
- package/lib/utils/filtering/filtering.test.ts +178 -0
- package/lib/utils/filtering/index.ts +20 -20
- package/lib/utils/sorting/index.ts +1 -1
- package/lib/utils/sorting/sorting.test.ts +88 -0
- package/package.json +12 -5
- package/lib/utils/virtualization/index.ts +0 -106
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { useLayout } from '../../hooks/core/useLayout';
|
|
3
|
+
import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavigation';
|
|
4
|
+
import { useGridControlledState } from '../../hooks/core/useGridControlledState';
|
|
5
|
+
import { useGridRowPipeline } from '../../hooks/core/useGridRowPipeline';
|
|
6
|
+
import { useGridVirtualization } from '../../hooks/core/useGridVirtualization';
|
|
7
|
+
import { useGridColumns } from '../../hooks/core/useGridColumns';
|
|
8
|
+
import { useGridVisibleRows } from '../../hooks/core/useGridVisibleRows';
|
|
9
|
+
import { useGridScrollSync } from '../../hooks/core/useGridScrollSync';
|
|
10
|
+
import { useGridStateSnapshot } from '../../hooks/core/useGridStateSnapshot';
|
|
11
|
+
import { GridAggregationFooter } from './GridAggregationFooter';
|
|
12
|
+
import { GridEmptyState } from './GridEmptyState';
|
|
13
|
+
import { GridErrorOverlay } from './GridErrorOverlay';
|
|
5
14
|
import { Header } from '../Header/Header';
|
|
6
|
-
import { Row } from '../Row/Row';
|
|
7
|
-
import { SkeletonRow } from '../SkeletonRow';
|
|
8
15
|
import { Pagination } from '../Pagination/Pagination';
|
|
9
16
|
import { useDataGrid } from '../../hooks/core/useDataGrid';
|
|
10
|
-
import { useColumnReorder } from '../../hooks/useColumnReorder';
|
|
11
17
|
import { useRowReorder } from '../../hooks/useRowReorder';
|
|
12
18
|
import { useTreeData } from '../../hooks/useTreeData';
|
|
13
19
|
import { useRowGrouping } from '../../hooks/useRowGrouping';
|
|
14
20
|
import { useGridEditing } from '../../hooks/features/useGridEditing';
|
|
15
21
|
import { useGridSpanning } from '../../hooks/features/useGridSpanning';
|
|
16
22
|
import { useGridDataSource } from '../../hooks/features/useGridDataSource';
|
|
17
|
-
import { useAggregation
|
|
23
|
+
import { useAggregation } from '../../hooks/features/useAggregation';
|
|
18
24
|
import { usePivot } from '../../hooks/features/usePivot';
|
|
19
25
|
import { useGridClipboard } from '../../hooks/features/useGridClipboard';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColumnOrderChangeParams, GridColDef, GridRenderCellParams, GridRowParams, GridCellParams, GridPaginationModel, GridDataSource, GridAggregationResult, GridAggregationModel, GridPivotModel, GridColumnPinning, GridFilterModel } from '../../types';
|
|
26
|
-
import '../../styles/opengridx.css';
|
|
26
|
+
import { GridListView } from './GridListView';
|
|
27
|
+
import { GridPinnedRows } from './GridPinnedRows';
|
|
28
|
+
import { GridVirtualRows } from './GridVirtualRows';
|
|
29
|
+
import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
|
|
30
|
+
import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem } from '../../types';
|
|
27
31
|
|
|
28
32
|
export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
|
|
29
33
|
const {
|
|
@@ -112,15 +116,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
112
116
|
const defaultFilterModel: GridFilterModel = useMemo(() => ({ items: [] }), []);
|
|
113
117
|
const filterModel = (propFilterModel || defaultFilterModel) as GridFilterModel;
|
|
114
118
|
|
|
115
|
-
const [internalSortModel, setInternalSortModel] = useState<{ field: string; sort: 'asc' | 'desc' }[]>(
|
|
116
|
-
() => initialState?.sorting?.sortModel ?? []
|
|
117
|
-
);
|
|
118
|
-
const isSortControlled = propSortModel !== undefined;
|
|
119
|
-
const sortModel = isSortControlled ? propSortModel : internalSortModel;
|
|
120
|
-
|
|
121
119
|
const defaultRowGroupingModel = useMemo(() => [], []);
|
|
122
120
|
const rowGroupingModel = propRowGroupingModel || defaultRowGroupingModel;
|
|
123
|
-
const isAggregationControlled = propAggregationModel !== undefined;
|
|
124
121
|
|
|
125
122
|
// ─── Stabilize toolbar component identity ────────────────────────────────
|
|
126
123
|
// Problem: demos/users often define their toolbar as an inline function
|
|
@@ -147,51 +144,46 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
147
144
|
const Toolbar = latestToolbarRef.current;
|
|
148
145
|
return Toolbar ? (Toolbar as (p: typeof props) => React.ReactElement | null)(props) : null;
|
|
149
146
|
}).current;
|
|
150
|
-
const [internalAggregationModel, setInternalAggregationModel] = useState<GridAggregationModel>(
|
|
151
|
-
() => propAggregationModel ?? {}
|
|
152
|
-
);
|
|
153
|
-
const aggregationModel = isAggregationControlled ? propAggregationModel : internalAggregationModel;
|
|
154
147
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
const controlledState = useGridControlledState({
|
|
149
|
+
initialState,
|
|
150
|
+
sortModel: propSortModel,
|
|
151
|
+
onSortModelChange,
|
|
152
|
+
aggregationModel: propAggregationModel,
|
|
153
|
+
onAggregationModelChange,
|
|
154
|
+
columnVisibilityModel: propColumnVisibilityModel,
|
|
155
|
+
onColumnVisibilityModelChange,
|
|
156
|
+
pinnedColumns: propPinnedColumns,
|
|
157
|
+
onPinnedColumnsChange,
|
|
158
|
+
pivotModel: propPivotModel,
|
|
159
|
+
onPivotModelChange,
|
|
160
|
+
paginationModel,
|
|
161
|
+
onPaginationModelChange,
|
|
162
|
+
rowSelectionModel: propRowSelectionModel,
|
|
163
|
+
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
164
|
+
});
|
|
167
165
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
const {
|
|
167
|
+
sortModel,
|
|
168
|
+
isSortControlled,
|
|
169
|
+
setInternalSortModel,
|
|
170
|
+
aggregationModel,
|
|
171
|
+
handleAggregationModelChange,
|
|
172
|
+
columnVisibilityModel,
|
|
173
|
+
handleColumnVisibilityModelChange,
|
|
174
|
+
pinnedColumns,
|
|
175
|
+
handlePinnedColumnsChange,
|
|
176
|
+
currentPivotModel,
|
|
177
|
+
handlePivotModelChange,
|
|
178
|
+
effectivePaginationModel,
|
|
179
|
+
handlePaginationModelChange,
|
|
180
|
+
selectedRowIds,
|
|
181
|
+
isSelectionControlled,
|
|
182
|
+
setInternalRowSelectionModel,
|
|
183
|
+
} = controlledState;
|
|
174
184
|
|
|
175
|
-
const
|
|
176
|
-
() => initialState?.columns?.pinnedColumns ?? {}
|
|
177
|
-
);
|
|
178
|
-
const isPinnedColumnsControlled = propPinnedColumns !== undefined;
|
|
179
|
-
const pinnedColumns = isPinnedColumnsControlled ? propPinnedColumns : internalPinnedColumns;
|
|
185
|
+
const onRowSelectionModelChange = propOnRowSelectionModelChange;
|
|
180
186
|
|
|
181
|
-
const handlePinnedColumnsChange = useCallback((model: GridColumnPinning) => {
|
|
182
|
-
if (!isPinnedColumnsControlled) {
|
|
183
|
-
setInternalPinnedColumns(model);
|
|
184
|
-
}
|
|
185
|
-
onPinnedColumnsChange?.(model);
|
|
186
|
-
}, [isPinnedColumnsControlled, onPinnedColumnsChange]);
|
|
187
|
-
|
|
188
|
-
const EMPTY_PIVOT_MODEL: GridPivotModel = useMemo(() => ({ rowFields: [], columnFields: [], valueFields: [] }), []);
|
|
189
|
-
const [internalPivotModel, setInternalPivotModel] = useState<GridPivotModel>(propPivotModel ?? EMPTY_PIVOT_MODEL);
|
|
190
|
-
const currentPivotModel = propPivotModel ?? internalPivotModel;
|
|
191
|
-
const handlePivotModelChange = useCallback((model: GridPivotModel) => {
|
|
192
|
-
setInternalPivotModel(model);
|
|
193
|
-
onPivotModelChange?.(model);
|
|
194
|
-
}, [onPivotModelChange]);
|
|
195
187
|
const pivot = usePivot(rows as GridRowModel[], columns as unknown as GridColDef[], currentPivotModel, pivotMode);
|
|
196
188
|
|
|
197
189
|
const activeRows = (pivotMode && pivot.isValid ? pivot.pivotRows : rows) as unknown as R[];
|
|
@@ -203,25 +195,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
203
195
|
setServerAggregationResults(null);
|
|
204
196
|
}, [aggregationModel]);
|
|
205
197
|
|
|
206
|
-
const [internalPaginationModel, setInternalPaginationModel] = useState(
|
|
207
|
-
() => initialState?.pagination?.paginationModel ?? paginationModel
|
|
208
|
-
);
|
|
209
|
-
const isPaginationControlled = props.paginationModel !== undefined;
|
|
210
|
-
const effectivePaginationModel = isPaginationControlled ? paginationModel : internalPaginationModel;
|
|
211
|
-
|
|
212
|
-
const handlePaginationModelChange = useCallback((newModel: GridPaginationModel) => {
|
|
213
|
-
if (!isPaginationControlled) {
|
|
214
|
-
setInternalPaginationModel(newModel);
|
|
215
|
-
}
|
|
216
|
-
onPaginationModelChange?.(newModel);
|
|
217
|
-
}, [isPaginationControlled, onPaginationModelChange]);
|
|
218
|
-
|
|
219
|
-
const [internalRowSelectionModel, setInternalRowSelectionModel] = useState<GridRowId[]>([]);
|
|
220
|
-
const isSelectionControlled = propRowSelectionModel !== undefined;
|
|
221
|
-
const rowSelectionModel = isSelectionControlled ? propRowSelectionModel : internalRowSelectionModel;
|
|
222
|
-
const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
|
|
223
|
-
const onRowSelectionModelChange = propOnRowSelectionModelChange;
|
|
224
|
-
|
|
225
198
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
226
199
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
227
200
|
const gridRef = useRef<HTMLDivElement>(null);
|
|
@@ -229,8 +202,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
229
202
|
const defaultGetRowId = useCallback((row: R) => row.id, []);
|
|
230
203
|
const effectiveGetRowId = getRowId || defaultGetRowId;
|
|
231
204
|
|
|
232
|
-
const [focusedCell, setFocusedCell] = useState<{ id: GridRowId; field: string } | null>(null);
|
|
233
|
-
|
|
234
205
|
// Keyboard-mode flag: toggled via DOM classname — no React state needed
|
|
235
206
|
// so the ring appears instantly without a re-render cycle.
|
|
236
207
|
const setKeyboardMode = useCallback((on: boolean) => {
|
|
@@ -241,12 +212,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
241
212
|
dataSourceRef.current = dataSource;
|
|
242
213
|
|
|
243
214
|
const fetchChildrenRef = useRef<((parentId: GridRowId, groupKeys: string[]) => Promise<void>) | null>(null);
|
|
244
|
-
const treeDataRef = useRef<
|
|
215
|
+
const treeDataRef = useRef<ReturnType<typeof useTreeData> | null>(null);
|
|
245
216
|
|
|
246
|
-
const handleNodeExpansion = useCallback((node:
|
|
217
|
+
const handleNodeExpansion = useCallback((node: GridTreeNode) => {
|
|
247
218
|
if (dataSourceRef.current && treeData) {
|
|
248
219
|
|
|
249
|
-
if (node.serverChildrenCount > 0 && node.children.length === 0) {
|
|
220
|
+
if ((node.serverChildrenCount ?? 0) > 0 && (node.children ?? []).length === 0) {
|
|
250
221
|
|
|
251
222
|
const groupKeys = treeDataRef.current?.getNodePath(node.id) || [node.groupingKey];
|
|
252
223
|
fetchChildrenRef.current?.(node.id, groupKeys);
|
|
@@ -268,13 +239,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
268
239
|
apiRef,
|
|
269
240
|
setRows,
|
|
270
241
|
setColumns,
|
|
271
|
-
setScroll,
|
|
272
242
|
setDimensions,
|
|
273
243
|
setDataSourceLoading,
|
|
274
244
|
setDataSourceError,
|
|
275
245
|
setRowCount
|
|
276
246
|
} = gridData;
|
|
277
247
|
|
|
248
|
+
const { scrollTop, scrollLeft, handleScroll } = useGridScrollSync({ onRowsScrollEnd });
|
|
249
|
+
|
|
278
250
|
useEffect(() => {
|
|
279
251
|
if (propApiRef) {
|
|
280
252
|
propApiRef.current = apiRef.current;
|
|
@@ -338,7 +310,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
338
310
|
const { dispatch } = gridData;
|
|
339
311
|
const currentRows = Array.from(gridData.state.rows.idRowsLookup.values());
|
|
340
312
|
const nextRows = currentRows.map(r =>
|
|
341
|
-
|
|
313
|
+
r.id === updatedRow.id ? updatedRow : r
|
|
342
314
|
);
|
|
343
315
|
dispatch({ type: 'SET_ROWS', payload: nextRows });
|
|
344
316
|
},
|
|
@@ -353,9 +325,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
353
325
|
|
|
354
326
|
const handleRowClick = useCallback((params: GridRowParams<R>) => {
|
|
355
327
|
const { row, id } = params;
|
|
356
|
-
const rowData = row as any;
|
|
357
328
|
|
|
358
|
-
if (isHierarchyEnabled &&
|
|
329
|
+
if (isHierarchyEnabled && row._hasChildren) {
|
|
359
330
|
activeHierarchyHandlers?.toggleExpansion(id);
|
|
360
331
|
return;
|
|
361
332
|
}
|
|
@@ -363,120 +334,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
363
334
|
onRowClick?.(params);
|
|
364
335
|
}, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick]);
|
|
365
336
|
|
|
366
|
-
const effectiveColumns = useMemo(() => {
|
|
367
|
-
if (!isHierarchyEnabled) return activeColumns;
|
|
368
|
-
|
|
369
|
-
return activeColumns.map((col, index) => {
|
|
370
|
-
|
|
371
|
-
if (index === 0) {
|
|
372
|
-
return {
|
|
373
|
-
...col,
|
|
374
|
-
renderCell: (params: GridRenderCellParams<R>) => {
|
|
375
|
-
const { _treeDepth, _hasChildren, _isExpanded, _groupingField, _groupingValue, _descendantCount } = params.row as any;
|
|
376
|
-
const depth = _treeDepth || 0;
|
|
377
|
-
const paddingLeft = depth * 24;
|
|
378
|
-
|
|
379
|
-
let content = col.renderCell ? col.renderCell(params) : params.value;
|
|
380
|
-
|
|
381
|
-
if (isTreeData && _hasChildren && params.row._isGroupRow) {
|
|
382
|
-
content = (
|
|
383
|
-
<div className="ogx__group-cell-content">
|
|
384
|
-
{params.value}
|
|
385
|
-
{_descendantCount !== undefined && _descendantCount > 0 ? ` (${_descendantCount})` : ''}
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// For row grouping, override the first column content to show the group label
|
|
391
|
-
if (isRowGrouping && _hasChildren && _groupingField) {
|
|
392
|
-
content = (
|
|
393
|
-
<div className="ogx__group-cell-content">
|
|
394
|
-
{col.field === _groupingField ? '' : `${_groupingField}: `}
|
|
395
|
-
{String(_groupingValue)}
|
|
396
|
-
{_descendantCount !== undefined ? ` (${_descendantCount})` : ''}
|
|
397
|
-
</div>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return (
|
|
402
|
-
<div style={{ display: 'flex', alignItems: 'center', paddingLeft, width: '100%', height: '100%' }}>
|
|
403
|
-
<div
|
|
404
|
-
style={{
|
|
405
|
-
marginRight: 4,
|
|
406
|
-
width: 24,
|
|
407
|
-
height: 24,
|
|
408
|
-
display: 'flex',
|
|
409
|
-
alignItems: 'center',
|
|
410
|
-
justifyContent: 'center',
|
|
411
|
-
cursor: _hasChildren ? 'pointer' : 'default',
|
|
412
|
-
flexShrink: 0
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
|
|
416
|
-
{_hasChildren ? (
|
|
417
|
-
<div
|
|
418
|
-
onClick={(e) => {
|
|
419
|
-
e.stopPropagation();
|
|
420
|
-
e.preventDefault();
|
|
421
|
-
activeHierarchyHandlers?.toggleExpansion(params.row.id);
|
|
422
|
-
}}
|
|
423
|
-
onMouseDown={(e) => {
|
|
424
|
-
e.stopPropagation();
|
|
425
|
-
}}
|
|
426
|
-
style={{
|
|
427
|
-
cursor: 'pointer',
|
|
428
|
-
display: 'flex',
|
|
429
|
-
alignItems: 'center',
|
|
430
|
-
justifyContent: 'center',
|
|
431
|
-
pointerEvents: 'auto',
|
|
432
|
-
zIndex: 10,
|
|
433
|
-
position: 'relative'
|
|
434
|
-
}}
|
|
435
|
-
>
|
|
436
|
-
<ExpandIcon
|
|
437
|
-
isExpanded={_isExpanded}
|
|
438
|
-
onClick={(e) => {
|
|
439
|
-
e.stopPropagation();
|
|
440
|
-
e.preventDefault();
|
|
441
|
-
activeHierarchyHandlers?.toggleExpansion(params.row.id);
|
|
442
|
-
}}
|
|
443
|
-
/>
|
|
444
|
-
</div>
|
|
445
|
-
) : null}
|
|
446
|
-
</div>
|
|
447
|
-
<div style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
448
|
-
{content}
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
...col,
|
|
458
|
-
renderCell: (params: GridRenderCellParams<R>) => {
|
|
459
|
-
const { _hasChildren, _groupingField } = params.row as any;
|
|
460
|
-
|
|
461
|
-
if (isRowGrouping && _hasChildren) {
|
|
462
|
-
|
|
463
|
-
if (col.field === _groupingField) {
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (params.value !== undefined && params.value !== null) {
|
|
468
|
-
return col.renderCell ? col.renderCell(params) : params.value;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return col.renderCell ? col.renderCell(params) : params.value;
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
}) as GridColDef<R>[];
|
|
478
|
-
}, [activeColumns, isHierarchyEnabled, isRowGrouping, activeHierarchyHandlers]);
|
|
479
|
-
|
|
480
337
|
const dataSourceHandlers = useGridDataSource({
|
|
481
338
|
dataSource,
|
|
482
339
|
sortModel,
|
|
@@ -499,68 +356,16 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
499
356
|
}
|
|
500
357
|
}, [dataSourceHandlers.fetchChildren]);
|
|
501
358
|
|
|
502
|
-
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
|
503
|
-
() => initialState?.columns?.columnWidths ?? {}
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(() =>
|
|
507
|
-
initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
const effectiveColumnOrder = columnOrder || internalColumnOrder;
|
|
511
|
-
|
|
512
359
|
useEffect(() => {
|
|
513
360
|
if (!dataSource) {
|
|
514
361
|
setRows(activeRows);
|
|
515
362
|
}
|
|
516
363
|
}, [activeRows, setRows, dataSource]);
|
|
517
364
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}, [activeColumns, setColumns]);
|
|
521
|
-
|
|
522
|
-
useEffect(() => {
|
|
523
|
-
if (pivotMode) {
|
|
524
|
-
setInternalColumnOrder(activeColumns.map(col => col.field));
|
|
525
|
-
}
|
|
526
|
-
}, [pivotMode, activeColumns]);
|
|
527
|
-
|
|
528
|
-
const orderedColumns = useMemo(() => {
|
|
529
|
-
if (disableColumnReorder) return effectiveColumns as GridColDef<R>[];
|
|
530
|
-
|
|
531
|
-
const orderMap = new Map(effectiveColumnOrder.map((field, index) => [field, index]));
|
|
532
|
-
return [...(effectiveColumns as GridColDef<R>[])].sort((a, b) => {
|
|
533
|
-
const aIndex = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
|
|
534
|
-
const bIndex = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
|
|
535
|
-
return aIndex - bIndex;
|
|
536
|
-
});
|
|
537
|
-
}, [effectiveColumns, effectiveColumnOrder, disableColumnReorder]);
|
|
538
|
-
|
|
539
|
-
const visibleOrderedColumns = useMemo(() => {
|
|
540
|
-
return orderedColumns.filter(col => columnVisibilityModel[col.field] !== false);
|
|
541
|
-
}, [orderedColumns, columnVisibilityModel]);
|
|
542
|
-
|
|
543
|
-
const columnReorderHandlers = useColumnReorder({
|
|
544
|
-
columns: orderedColumns,
|
|
545
|
-
onColumnOrderChange: useCallback((params: GridColumnOrderChangeParams) => {
|
|
546
|
-
const { oldIndex, targetIndex } = params;
|
|
547
|
-
|
|
548
|
-
const newOrder = [...effectiveColumnOrder];
|
|
549
|
-
const [movedField] = newOrder.splice(oldIndex, 1);
|
|
550
|
-
newOrder.splice(targetIndex, 0, movedField);
|
|
551
|
-
|
|
552
|
-
if (!columnOrder) {
|
|
553
|
-
setInternalColumnOrder(newOrder);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
onColumnOrderChange?.(params);
|
|
557
|
-
}, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
|
|
558
|
-
disableColumnReorder,
|
|
559
|
-
});
|
|
560
|
-
|
|
365
|
+
// ── Detail panel (hoisted — hasDetailPanel feeds into useGridColumns) ──────
|
|
366
|
+
const hasDetailPanel = Boolean(getDetailPanelContent);
|
|
561
367
|
const [internalExpandedRowIds, setInternalExpandedRowIds] = useState<Set<GridRowId>>(new Set());
|
|
562
368
|
const expandedRowIds = controlledExpandedRowIds ?? internalExpandedRowIds;
|
|
563
|
-
const hasDetailPanel = Boolean(getDetailPanelContent);
|
|
564
369
|
|
|
565
370
|
const handleDetailPanelToggle = useCallback((rowId: GridRowId) => {
|
|
566
371
|
const newExpandedRowIds = new Set(expandedRowIds);
|
|
@@ -569,45 +374,43 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
569
374
|
} else {
|
|
570
375
|
newExpandedRowIds.add(rowId);
|
|
571
376
|
}
|
|
572
|
-
|
|
573
377
|
if (controlledExpandedRowIds === undefined) {
|
|
574
378
|
setInternalExpandedRowIds(newExpandedRowIds);
|
|
575
379
|
}
|
|
576
380
|
onDetailPanelExpandedRowIdsChange?.(newExpandedRowIds);
|
|
577
381
|
}, [expandedRowIds, controlledExpandedRowIds, onDetailPanelExpandedRowIdsChange]);
|
|
578
382
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
};
|
|
383
|
+
// ── Column management ─────────────────────────────────────────────────────
|
|
384
|
+
const {
|
|
385
|
+
effectiveColumns,
|
|
386
|
+
orderedColumns,
|
|
387
|
+
visibleOrderedColumns,
|
|
388
|
+
navigationColumns,
|
|
389
|
+
columnWidths,
|
|
390
|
+
effectiveColumnOrder,
|
|
391
|
+
setInternalColumnOrder,
|
|
392
|
+
columnReorderHandlers,
|
|
393
|
+
handleColumnResize,
|
|
394
|
+
} = useGridColumns<R>({
|
|
395
|
+
activeColumns,
|
|
396
|
+
isHierarchyEnabled,
|
|
397
|
+
isRowGrouping,
|
|
398
|
+
isTreeData,
|
|
399
|
+
activeHierarchyHandlers,
|
|
400
|
+
columnVisibilityModel,
|
|
401
|
+
columnOrder,
|
|
402
|
+
onColumnOrderChange,
|
|
403
|
+
disableColumnReorder,
|
|
404
|
+
pivotMode,
|
|
405
|
+
checkboxSelection,
|
|
406
|
+
hasDetailPanel,
|
|
407
|
+
rowReordering,
|
|
408
|
+
initialState,
|
|
409
|
+
setColumns,
|
|
410
|
+
});
|
|
608
411
|
|
|
609
|
-
|
|
610
|
-
|
|
412
|
+
useGridStateSnapshot({
|
|
413
|
+
onStateChange,
|
|
611
414
|
sortModel,
|
|
612
415
|
filterModel,
|
|
613
416
|
effectivePaginationModel,
|
|
@@ -615,61 +418,34 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
615
418
|
effectiveColumnOrder,
|
|
616
419
|
columnVisibilityModel,
|
|
617
420
|
pinnedColumns,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const baseRows = useMemo(() => {
|
|
621
|
-
return effectiveRows;
|
|
622
|
-
}, [effectiveRows]);
|
|
623
|
-
|
|
624
|
-
const filteredRows = useMemo(() => {
|
|
625
|
-
if (activeHierarchyHandlers) {
|
|
626
|
-
|
|
627
|
-
return (activeHierarchyHandlers.getVisibleRows() || []) as R[];
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (filterMode === 'server' && dataSource) {
|
|
631
|
-
return baseRows;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return filterRows(baseRows, filterModel);
|
|
635
|
-
}, [baseRows, filterModel, activeHierarchyHandlers, filterMode, dataSource]);
|
|
636
|
-
|
|
637
|
-
const { top: pinnedTopRows, center: unpinnedRows, bottom: pinnedBottomRows } = useMemo(() => {
|
|
638
|
-
if (activeHierarchyHandlers) {
|
|
639
|
-
|
|
640
|
-
return { top: [], center: filteredRows, bottom: [] };
|
|
641
|
-
}
|
|
642
|
-
return getPinnedRowGroups(filteredRows, pinnedRows);
|
|
643
|
-
}, [filteredRows, pinnedRows, activeHierarchyHandlers]);
|
|
644
|
-
|
|
645
|
-
const sortedUnpinnedRows = useMemo(() => {
|
|
646
|
-
if (activeHierarchyHandlers) {
|
|
647
|
-
return unpinnedRows;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (sortingMode === 'server' && dataSource) {
|
|
651
|
-
return unpinnedRows;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return sortRows(unpinnedRows, sortModel);
|
|
655
|
-
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, dataSource]);
|
|
656
|
-
|
|
657
|
-
const paginatedUnpinnedRows = useMemo(() => {
|
|
658
|
-
if (!pagination) return sortedUnpinnedRows;
|
|
659
|
-
|
|
660
|
-
if (paginationMode === 'server' && dataSource) {
|
|
661
|
-
return sortedUnpinnedRows;
|
|
662
|
-
}
|
|
421
|
+
});
|
|
663
422
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
423
|
+
const rowPipeline = useGridRowPipeline<GridRowModel>({
|
|
424
|
+
effectiveRows: effectiveRows as GridRowModel[],
|
|
425
|
+
activeHierarchyHandlers: activeHierarchyHandlers as { getVisibleRows: () => GridRowModel[] } | null,
|
|
426
|
+
filterMode,
|
|
427
|
+
filterModel,
|
|
428
|
+
dataSource: dataSource as GridDataSource<GridRowModel> | undefined,
|
|
429
|
+
sortModel,
|
|
430
|
+
sortingMode,
|
|
431
|
+
pagination,
|
|
432
|
+
paginationMode,
|
|
433
|
+
effectivePaginationModel,
|
|
434
|
+
pinnedRows,
|
|
435
|
+
isLoading: state.dataSource.loading,
|
|
436
|
+
pageSize: effectivePaginationModel.pageSize,
|
|
437
|
+
});
|
|
438
|
+
const filteredRows = rowPipeline.filteredRows as R[];
|
|
439
|
+
const pinnedTopRows = rowPipeline.pinnedTopRows as R[];
|
|
440
|
+
const pinnedBottomRows = rowPipeline.pinnedBottomRows as R[];
|
|
441
|
+
const sortedUnpinnedRows = rowPipeline.sortedUnpinnedRows as R[];
|
|
442
|
+
const paginatedUnpinnedRows = rowPipeline.paginatedUnpinnedRows as R[];
|
|
443
|
+
const allRenderableRows = rowPipeline.allRenderableRows as R[];
|
|
668
444
|
|
|
669
445
|
useEffect(() => {
|
|
670
|
-
gridData.apiRef.current.getVisibleRows = () => sortedUnpinnedRows;
|
|
671
|
-
gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as
|
|
672
|
-
}, [sortedUnpinnedRows, effectiveColumns, gridData.apiRef]);
|
|
446
|
+
gridData.apiRef.current.getVisibleRows = () => pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
447
|
+
gridData.apiRef.current.getVisibleColumns = () => effectiveColumns as unknown as GridColDef[];
|
|
448
|
+
}, [sortedUnpinnedRows, paginatedUnpinnedRows, pagination, effectiveColumns, gridData.apiRef]);
|
|
673
449
|
|
|
674
450
|
const rowReorderHandlers = useRowReorder({
|
|
675
451
|
rows: pagination ? paginatedUnpinnedRows : sortedUnpinnedRows,
|
|
@@ -678,29 +454,6 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
678
454
|
rowReordering
|
|
679
455
|
});
|
|
680
456
|
|
|
681
|
-
const allRenderableRows = useMemo(() => {
|
|
682
|
-
|
|
683
|
-
if (activeHierarchyHandlers) {
|
|
684
|
-
return unpinnedRows;
|
|
685
|
-
}
|
|
686
|
-
const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
687
|
-
const base = [...pinnedTopRows, ...centerRows, ...pinnedBottomRows];
|
|
688
|
-
|
|
689
|
-
// Append skeleton rows while an infinite-scroll fetch is in flight
|
|
690
|
-
if (paginationMode === 'infinite' && state.dataSource.loading && base.length > 0) {
|
|
691
|
-
const skeletonCount = Math.min(effectivePaginationModel.pageSize, 20);
|
|
692
|
-
const skeletons = Array.from({ length: skeletonCount }, (_, i) => ({
|
|
693
|
-
id: `__skeleton_${i}__`,
|
|
694
|
-
_isSkeleton: true,
|
|
695
|
-
}));
|
|
696
|
-
return [...base, ...skeletons] as typeof base;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
return base;
|
|
700
|
-
}, [pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows, pagination,
|
|
701
|
-
activeHierarchyHandlers, unpinnedRows, paginationMode, state.dataSource.loading,
|
|
702
|
-
effectivePaginationModel.pageSize]);
|
|
703
|
-
|
|
704
457
|
const spanning = useGridSpanning(
|
|
705
458
|
allRenderableRows,
|
|
706
459
|
navigationColumns,
|
|
@@ -711,7 +464,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
711
464
|
rows: filteredRows,
|
|
712
465
|
aggregationModel,
|
|
713
466
|
isServerSide: !!(dataSource && (paginationMode === 'server' || sortingMode === 'server')),
|
|
714
|
-
dataSource
|
|
467
|
+
dataSource,
|
|
715
468
|
filterModel,
|
|
716
469
|
sortModel,
|
|
717
470
|
serverAggregationResults,
|
|
@@ -724,325 +477,19 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
724
477
|
gridData.apiRef.current.getAggregationModel = () => hasAggregation ? aggregationModel : null;
|
|
725
478
|
}, [aggregationResult, aggregationModel, hasAggregation, gridData.apiRef]);
|
|
726
479
|
|
|
727
|
-
const layout =
|
|
728
|
-
const baseRowHeight = rowHeight;
|
|
729
|
-
|
|
730
|
-
const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
731
|
-
const rowHeights = unpinnedRows.map((row, index) => {
|
|
732
|
-
let height = baseRowHeight;
|
|
733
|
-
if (expandedRowIds.has(row.id)) {
|
|
734
|
-
const detailHeight = getDetailPanelHeight?.({ row, id: row.id, rowIndex: pinnedTopRows.length + index }) ?? 200;
|
|
735
|
-
height += typeof detailHeight === 'number' ? detailHeight : parseInt(String(detailHeight), 10) || 200;
|
|
736
|
-
}
|
|
737
|
-
return height;
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const cumulativeHeights = rowHeights.reduce((acc, height, index) => {
|
|
741
|
-
acc.push((acc[index - 1] || 0) + height);
|
|
742
|
-
return acc;
|
|
743
|
-
}, [] as number[]);
|
|
744
|
-
|
|
745
|
-
const skeletonCount = (paginationMode === 'infinite' && state.dataSource.loading && (unpinnedRows.length > 0))
|
|
746
|
-
? Math.min(effectivePaginationModel.pageSize, 20)
|
|
747
|
-
: 0;
|
|
748
|
-
const skeletonRowsHeight = skeletonCount * baseRowHeight;
|
|
749
|
-
const unpinnedRowsHeight = (cumulativeHeights[cumulativeHeights.length - 1] || 0) + skeletonRowsHeight;
|
|
750
|
-
const pinnedTopHeight = pinnedTopRows.length * baseRowHeight;
|
|
751
|
-
const pinnedBottomHeight = pinnedBottomRows.length * baseRowHeight;
|
|
752
|
-
|
|
753
|
-
const rawLeftPinnedCols: GridColDef<R>[] = [];
|
|
754
|
-
const rawRightPinnedCols: GridColDef<R>[] = [];
|
|
755
|
-
const unpinnedCols: GridColDef<R>[] = [];
|
|
756
|
-
const unpinnedColWidths: number[] = [];
|
|
757
|
-
|
|
758
|
-
visibleOrderedColumns.forEach(col => {
|
|
759
|
-
const pinned = isColumnPinned(col.field, pinnedColumns);
|
|
760
|
-
if (pinned === 'left') {
|
|
761
|
-
rawLeftPinnedCols.push(col);
|
|
762
|
-
} else if (pinned === 'right') {
|
|
763
|
-
rawRightPinnedCols.push(col);
|
|
764
|
-
} else {
|
|
765
|
-
unpinnedCols.push(col);
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
const viewportWidth = state.dimensions.viewportWidth || 1000;
|
|
770
|
-
const systemColumnsWidth = (checkboxSelection ? 48 : 0) + (hasDetailPanel ? 48 : 0) + (rowReordering ? 48 : 0);
|
|
771
|
-
|
|
772
|
-
const getColWidth = (c: GridColDef<R>) => columnWidths[c.field] ?? c.width;
|
|
773
|
-
|
|
774
|
-
const parseWidth = (width: number | string | undefined): { type: 'fixed' | 'percentage' | 'auto', value: number } => {
|
|
775
|
-
if (width === undefined) return { type: 'auto', value: 0 };
|
|
776
|
-
if (typeof width === 'number') return { type: 'fixed', value: width };
|
|
777
|
-
if (typeof width === 'string') {
|
|
778
|
-
if (width.toLowerCase() === 'auto') return { type: 'auto', value: 0 };
|
|
779
|
-
if (width.endsWith('%')) {
|
|
780
|
-
const percentage = parseFloat(width);
|
|
781
|
-
return { type: 'percentage', value: percentage };
|
|
782
|
-
}
|
|
783
|
-
const floatVal = parseFloat(width);
|
|
784
|
-
if (!isNaN(floatVal)) return { type: 'fixed', value: floatVal };
|
|
785
|
-
}
|
|
786
|
-
return { type: 'fixed', value: 100 };
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const leftWidth = rawLeftPinnedCols.reduce((sum, c) => {
|
|
790
|
-
const parsed = parseWidth(getColWidth(c));
|
|
791
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
792
|
-
}, 0);
|
|
793
|
-
const rightWidth = rawRightPinnedCols.reduce((sum, c) => {
|
|
794
|
-
const parsed = parseWidth(getColWidth(c));
|
|
795
|
-
return sum + (parsed.type === 'fixed' ? parsed.value : 100);
|
|
796
|
-
}, 0);
|
|
797
|
-
|
|
798
|
-
const naturalFlexWidth = unpinnedCols.reduce((sum, col) => {
|
|
799
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
800
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
801
|
-
const minWidth = col.minWidth ?? 150;
|
|
802
|
-
const width = typeof col.width === 'number' ? col.width : 150;
|
|
803
|
-
return sum + (minWidth || width);
|
|
804
|
-
}
|
|
805
|
-
const parsed = parseWidth(getColWidth(col));
|
|
806
|
-
if (parsed.type === 'fixed') return sum + parsed.value;
|
|
807
|
-
if (parsed.type === 'percentage') return sum + 100;
|
|
808
|
-
return sum + (col.minWidth ?? 150);
|
|
809
|
-
}, 0);
|
|
810
|
-
|
|
811
|
-
const availableWidth = Math.max(
|
|
812
|
-
viewportWidth - systemColumnsWidth - leftWidth - rightWidth,
|
|
813
|
-
naturalFlexWidth
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
const fixedWidthCols: Array<{ col: GridColDef<R>, width: number }> = [];
|
|
817
|
-
const percentageCols: Array<{ col: GridColDef<R>, percentage: number }> = [];
|
|
818
|
-
const flexCols: Array<{ col: GridColDef<R>, flex: number }> = [];
|
|
819
|
-
let totalFlexUnits = 0;
|
|
820
|
-
|
|
821
|
-
unpinnedCols.forEach(col => {
|
|
822
|
-
// If the user has manually resized this column, columnWidths has an explicit
|
|
823
|
-
// entry for it. Respect that stored width as fixed-width so the resize sticks,
|
|
824
|
-
// even if the column definition originally had flex.
|
|
825
|
-
const hasManualWidth = Object.prototype.hasOwnProperty.call(columnWidths, col.field);
|
|
826
|
-
|
|
827
|
-
const parsed = parseWidth(getColWidth(col));
|
|
828
|
-
|
|
829
|
-
if (!hasManualWidth && col.flex && col.flex > 0) {
|
|
830
|
-
flexCols.push({ col, flex: col.flex });
|
|
831
|
-
totalFlexUnits += col.flex;
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
if (parsed.type === 'fixed') {
|
|
836
|
-
fixedWidthCols.push({ col, width: parsed.value });
|
|
837
|
-
} else if (parsed.type === 'percentage') {
|
|
838
|
-
percentageCols.push({ col, percentage: parsed.value });
|
|
839
|
-
} else {
|
|
840
|
-
|
|
841
|
-
const implicitFlex = 1;
|
|
842
|
-
flexCols.push({ col, flex: implicitFlex });
|
|
843
|
-
totalFlexUnits += implicitFlex;
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
const fixedWidth = fixedWidthCols.reduce((sum, { width }) => sum + width, 0);
|
|
848
|
-
|
|
849
|
-
let remainingSpaceForFlex = Math.max(0, availableWidth - fixedWidth);
|
|
850
|
-
|
|
851
|
-
const percentageWidthMap = new Map<string, number>();
|
|
852
|
-
percentageCols.forEach(({ col, percentage }) => {
|
|
853
|
-
const calculatedWidth = (percentage / 100) * remainingSpaceForFlex;
|
|
854
|
-
percentageWidthMap.set(col.field, calculatedWidth);
|
|
855
|
-
|
|
856
|
-
remainingSpaceForFlex -= calculatedWidth;
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
remainingSpaceForFlex = Math.max(0, remainingSpaceForFlex);
|
|
860
|
-
|
|
861
|
-
const finalFlexWidths = new Map<string, number>();
|
|
862
|
-
|
|
863
|
-
const flexItems = flexCols.map(f => ({
|
|
864
|
-
...f,
|
|
865
|
-
minWidth: f.col.minWidth ?? 50,
|
|
866
|
-
maxWidth: f.col.maxWidth ?? Infinity,
|
|
867
|
-
frozen: false,
|
|
868
|
-
computedWidth: 0
|
|
869
|
-
}));
|
|
870
|
-
|
|
871
|
-
const solveFlexAllocation = () => {
|
|
872
|
-
|
|
873
|
-
let iterations = 0;
|
|
874
|
-
const maxIterations = flexItems.length * 2;
|
|
875
|
-
|
|
876
|
-
while (iterations < maxIterations) {
|
|
877
|
-
iterations++;
|
|
878
|
-
|
|
879
|
-
const unfrozen = flexItems.filter(f => !f.frozen);
|
|
880
|
-
|
|
881
|
-
if (unfrozen.length === 0) break;
|
|
882
|
-
|
|
883
|
-
const unfrozenFlexTotal = unfrozen.reduce((sum, f) => sum + f.flex, 0);
|
|
884
|
-
|
|
885
|
-
const frozenWidthTotal = flexItems.reduce((sum, f) => f.frozen ? sum + f.computedWidth : sum, 0);
|
|
886
|
-
const currentFreeSpace = Math.max(0, remainingSpaceForFlex - frozenWidthTotal);
|
|
887
|
-
|
|
888
|
-
if (unfrozenFlexTotal <= 0) {
|
|
889
|
-
unfrozen.forEach(f => {
|
|
890
|
-
f.computedWidth = f.minWidth;
|
|
891
|
-
f.frozen = true;
|
|
892
|
-
});
|
|
893
|
-
break;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const pixelsPerFlex = currentFreeSpace / unfrozenFlexTotal;
|
|
897
|
-
|
|
898
|
-
let totalViolation = 0;
|
|
899
|
-
const minViolators: typeof flexItems = [];
|
|
900
|
-
const maxViolators: typeof flexItems = [];
|
|
901
|
-
|
|
902
|
-
unfrozen.forEach(f => {
|
|
903
|
-
const rawWidth = pixelsPerFlex * f.flex;
|
|
904
|
-
|
|
905
|
-
if (rawWidth < f.minWidth) {
|
|
906
|
-
const diff = f.minWidth - rawWidth;
|
|
907
|
-
totalViolation += diff;
|
|
908
|
-
minViolators.push(f);
|
|
909
|
-
} else if (rawWidth > f.maxWidth) {
|
|
910
|
-
const diff = f.maxWidth - rawWidth;
|
|
911
|
-
totalViolation += diff;
|
|
912
|
-
maxViolators.push(f);
|
|
913
|
-
} else {
|
|
914
|
-
|
|
915
|
-
f.computedWidth = rawWidth;
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
if (minViolators.length === 0 && maxViolators.length === 0) {
|
|
920
|
-
unfrozen.forEach(f => f.frozen = true);
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (totalViolation > 0) {
|
|
925
|
-
minViolators.forEach(f => {
|
|
926
|
-
f.computedWidth = f.minWidth;
|
|
927
|
-
f.frozen = true;
|
|
928
|
-
});
|
|
929
|
-
} else if (totalViolation < 0) {
|
|
930
|
-
maxViolators.forEach(f => {
|
|
931
|
-
f.computedWidth = f.maxWidth;
|
|
932
|
-
f.frozen = true;
|
|
933
|
-
});
|
|
934
|
-
} else {
|
|
935
|
-
|
|
936
|
-
minViolators.forEach(f => { f.computedWidth = f.minWidth; f.frozen = true; });
|
|
937
|
-
maxViolators.forEach(f => { f.computedWidth = f.maxWidth; f.frozen = true; });
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
solveFlexAllocation();
|
|
943
|
-
flexItems.forEach(f => finalFlexWidths.set(f.col.field, f.computedWidth));
|
|
944
|
-
|
|
945
|
-
const computedWidthMap = new Map<string, number>();
|
|
946
|
-
|
|
947
|
-
unpinnedCols.forEach(col => {
|
|
948
|
-
let computedWidth: number;
|
|
949
|
-
|
|
950
|
-
if (finalFlexWidths.has(col.field)) {
|
|
951
|
-
computedWidth = finalFlexWidths.get(col.field)!;
|
|
952
|
-
} else {
|
|
953
|
-
const fixedCol = fixedWidthCols.find(f => f.col.field === col.field);
|
|
954
|
-
const pctCol = percentageCols.find(p => p.col.field === col.field);
|
|
955
|
-
|
|
956
|
-
if (fixedCol) {
|
|
957
|
-
computedWidth = fixedCol.width;
|
|
958
|
-
} else if (pctCol) {
|
|
959
|
-
const rawPctWidth = percentageWidthMap.get(col.field) || 0;
|
|
960
|
-
computedWidth = Math.max(col.minWidth ?? 50, rawPctWidth || 100);
|
|
961
|
-
} else {
|
|
962
|
-
computedWidth = 100;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
unpinnedColWidths.push(computedWidth);
|
|
967
|
-
computedWidthMap.set(col.field, computedWidth);
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
const unpinnedColsWithWidth = unpinnedCols.map(col => {
|
|
971
|
-
const flexItem = flexItems.find(f => f.col.field === col.field);
|
|
972
|
-
return {
|
|
973
|
-
...col,
|
|
974
|
-
width: computedWidthMap.get(col.field) ?? (typeof col.width === 'number' ? col.width : 100),
|
|
975
|
-
flex: flexItem ? flexItem.flex : col.flex
|
|
976
|
-
};
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const baseLeftZ = 11;
|
|
980
|
-
const leftPinnedCols = rawLeftPinnedCols.map((col, i) => {
|
|
981
|
-
const parsed = parseWidth(getColWidth(col));
|
|
982
|
-
const numericWidth = parsed.type === 'fixed' ? parsed.value : 100;
|
|
983
|
-
return {
|
|
984
|
-
...col,
|
|
985
|
-
width: numericWidth,
|
|
986
|
-
zIndex: baseLeftZ + i
|
|
987
|
-
};
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
const baseRightZ = 11;
|
|
991
|
-
const rightPinnedCols = rawRightPinnedCols.map((col, i) => {
|
|
992
|
-
const parsed = parseWidth(getColWidth(col));
|
|
993
|
-
const numericWidth = parsed.type === 'fixed' ? parsed.value : 100;
|
|
994
|
-
return {
|
|
995
|
-
...col,
|
|
996
|
-
width: numericWidth,
|
|
997
|
-
zIndex: baseRightZ + (rawRightPinnedCols.length - 1 - i)
|
|
998
|
-
};
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
const unpinnedTotalWidth = unpinnedColWidths.reduce((sum, w) => sum + w, 0);
|
|
1002
|
-
const totalWidth = leftWidth + unpinnedTotalWidth + rightWidth + systemColumnsWidth;
|
|
1003
|
-
|
|
1004
|
-
const unpinnedAccWidths = unpinnedColWidths.reduce((acc, w, i) => {
|
|
1005
|
-
acc.push((acc[i - 1] || 0) + w);
|
|
1006
|
-
return acc;
|
|
1007
|
-
}, [] as number[]);
|
|
1008
|
-
|
|
1009
|
-
return {
|
|
1010
|
-
rowHeights,
|
|
1011
|
-
cumulativeHeights,
|
|
1012
|
-
unpinnedRowsHeight,
|
|
1013
|
-
pinnedTopHeight,
|
|
1014
|
-
pinnedBottomHeight,
|
|
1015
|
-
leftPinnedCols,
|
|
1016
|
-
rightPinnedCols,
|
|
1017
|
-
unpinnedColsWithWidth,
|
|
1018
|
-
unpinnedColWidths,
|
|
1019
|
-
unpinnedAccWidths,
|
|
1020
|
-
totalWidth,
|
|
1021
|
-
leftWidth,
|
|
1022
|
-
rightWidth,
|
|
1023
|
-
unpinnedTotalWidth,
|
|
1024
|
-
systemColumnsWidth,
|
|
1025
|
-
pinnedTopRowsLength: pinnedTopRows.length,
|
|
1026
|
-
pinnedBottomRowsLength: pinnedBottomRows.length,
|
|
1027
|
-
unpinnedRowsLength: unpinnedRows.length
|
|
1028
|
-
};
|
|
1029
|
-
}, [
|
|
480
|
+
const layout = useLayout({
|
|
1030
481
|
rowHeight,
|
|
1031
482
|
pagination,
|
|
1032
483
|
paginatedUnpinnedRows,
|
|
1033
484
|
sortedUnpinnedRows,
|
|
1034
485
|
expandedRowIds,
|
|
1035
486
|
getDetailPanelHeight,
|
|
1036
|
-
pinnedTopRows.length,
|
|
1037
|
-
pinnedBottomRows.length,
|
|
1038
|
-
expandedRowIds,
|
|
1039
|
-
getDetailPanelHeight,
|
|
1040
|
-
pinnedTopRows.length,
|
|
1041
|
-
pinnedBottomRows.length,
|
|
487
|
+
pinnedTopRowsLength: pinnedTopRows.length,
|
|
488
|
+
pinnedBottomRowsLength: pinnedBottomRows.length,
|
|
1042
489
|
visibleOrderedColumns,
|
|
1043
490
|
pinnedColumns,
|
|
1044
491
|
columnWidths,
|
|
1045
|
-
state.dimensions.viewportWidth,
|
|
492
|
+
viewportWidth: state.dimensions.viewportWidth || 1000,
|
|
1046
493
|
checkboxSelection,
|
|
1047
494
|
hasDetailPanel,
|
|
1048
495
|
rowReordering,
|
|
@@ -1050,152 +497,60 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1050
497
|
pinExpandColumn,
|
|
1051
498
|
autoHeight,
|
|
1052
499
|
paginationMode,
|
|
1053
|
-
state.dataSource.loading,
|
|
1054
|
-
effectivePaginationModel.pageSize
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const virtualization = useMemo(() => {
|
|
1058
|
-
const {
|
|
1059
|
-
unpinnedAccWidths,
|
|
1060
|
-
unpinnedColsWithWidth,
|
|
1061
|
-
leftPinnedCols,
|
|
1062
|
-
rightPinnedCols,
|
|
1063
|
-
totalWidth,
|
|
1064
|
-
leftWidth,
|
|
1065
|
-
rightWidth,
|
|
1066
|
-
unpinnedTotalWidth,
|
|
1067
|
-
systemColumnsWidth,
|
|
1068
|
-
cumulativeHeights,
|
|
1069
|
-
pinnedTopHeight,
|
|
1070
|
-
pinnedBottomHeight,
|
|
1071
|
-
unpinnedRowsHeight,
|
|
1072
|
-
rowHeights,
|
|
1073
|
-
unpinnedRowsLength
|
|
1074
|
-
} = layout;
|
|
1075
|
-
|
|
1076
|
-
const scrollLeft = state.virtualization.scrollLeft;
|
|
1077
|
-
const scrollTop = state.virtualization.scrollTop;
|
|
1078
|
-
const viewportWidth = state.dimensions.viewportWidth || 1000;
|
|
1079
|
-
const viewportHeight = autoHeight ? (pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + 50) : (state.dimensions.viewportHeight || 600);
|
|
1080
|
-
|
|
1081
|
-
const overscanCols = 6;
|
|
1082
|
-
|
|
1083
|
-
const visibleLocalStart = Math.max(0, scrollLeft - leftWidth);
|
|
1084
|
-
const visibleLocalEnd = scrollLeft + viewportWidth - leftWidth;
|
|
1085
|
-
|
|
1086
|
-
let firstUnpinnedIndex = 0;
|
|
1087
|
-
let lastUnpinnedIndex = unpinnedColsWithWidth.length - 1;
|
|
1088
|
-
|
|
1089
|
-
let low = 0, high = unpinnedAccWidths.length - 1;
|
|
1090
|
-
while (low <= high) {
|
|
1091
|
-
const mid = (low + high) >>> 1;
|
|
1092
|
-
if (unpinnedAccWidths[mid] < visibleLocalStart) {
|
|
1093
|
-
low = mid + 1;
|
|
1094
|
-
} else {
|
|
1095
|
-
high = mid - 1;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
firstUnpinnedIndex = Math.max(0, low - overscanCols);
|
|
1099
|
-
|
|
1100
|
-
for (let i = firstUnpinnedIndex; i < unpinnedAccWidths.length; i++) {
|
|
1101
|
-
if ((i === 0 ? 0 : unpinnedAccWidths[i - 1]) > visibleLocalEnd) {
|
|
1102
|
-
lastUnpinnedIndex = Math.min(unpinnedColsWithWidth.length - 1, i + overscanCols);
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
lastUnpinnedIndex = Math.min(unpinnedColsWithWidth.length - 1, i + overscanCols);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const leftSpacerWidth = firstUnpinnedIndex > 0 ? unpinnedAccWidths[firstUnpinnedIndex - 1] : 0;
|
|
1109
|
-
const rightSpacerWidth = unpinnedTotalWidth - unpinnedAccWidths[lastUnpinnedIndex];
|
|
1110
|
-
|
|
1111
|
-
const virtualColumns: any[] = [
|
|
1112
|
-
...leftPinnedCols,
|
|
1113
|
-
...(leftSpacerWidth > 0 ? [{ field: '__spacer_left__', width: leftSpacerWidth, isSpacer: true }] : []),
|
|
1114
|
-
...unpinnedColsWithWidth.slice(firstUnpinnedIndex, lastUnpinnedIndex + 1),
|
|
1115
|
-
...(rightSpacerWidth > 0 ? [{ field: '__spacer_right__', width: rightSpacerWidth, isSpacer: true }] : []),
|
|
1116
|
-
...rightPinnedCols
|
|
1117
|
-
];
|
|
1118
|
-
|
|
1119
|
-
const hasHorizontalScroll = viewportWidth > 0 && totalWidth > viewportWidth;
|
|
1120
|
-
const scrollbarBuffer = hasHorizontalScroll ? 16 : 0;
|
|
1121
|
-
const totalHeight = pinnedTopHeight + unpinnedRowsHeight + pinnedBottomHeight + scrollbarBuffer + 2;
|
|
1122
|
-
|
|
1123
|
-
const overscanRows = 5;
|
|
1124
|
-
let firstRowIndex = 0;
|
|
1125
|
-
let lastRowIndex = unpinnedRowsLength - 1;
|
|
1126
|
-
let offsetTop = 0;
|
|
1127
|
-
|
|
1128
|
-
let rLow = 0, rHigh = cumulativeHeights.length - 1;
|
|
1129
|
-
while (rLow <= rHigh) {
|
|
1130
|
-
const mid = (rLow + rHigh) >>> 1;
|
|
1131
|
-
if (cumulativeHeights[mid] < scrollTop) {
|
|
1132
|
-
rLow = mid + 1;
|
|
1133
|
-
} else {
|
|
1134
|
-
rHigh = mid - 1;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
firstRowIndex = Math.max(0, rLow - overscanRows);
|
|
1138
|
-
offsetTop = firstRowIndex > 0 ? cumulativeHeights[firstRowIndex - 1] : 0;
|
|
500
|
+
isLoading: state.dataSource.loading,
|
|
501
|
+
pageSize: effectivePaginationModel.pageSize,
|
|
502
|
+
});
|
|
1139
503
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
gridData.apiRef.current.scrollToIndexes = ({ rowIndex, colIndex }) => {
|
|
506
|
+
const el = viewportRef.current;
|
|
507
|
+
if (!el) return;
|
|
508
|
+
|
|
509
|
+
if (rowIndex !== undefined && rowIndex >= 0) {
|
|
510
|
+
const rowTop = rowIndex > 0 ? layout.cumulativeHeights[rowIndex - 1] : 0;
|
|
511
|
+
const rowBottom = layout.cumulativeHeights[rowIndex] ?? rowTop;
|
|
512
|
+
const { scrollTop, clientHeight } = el;
|
|
513
|
+
if (rowTop < scrollTop) {
|
|
514
|
+
el.scrollTop = rowTop;
|
|
515
|
+
} else if (rowBottom > scrollTop + clientHeight) {
|
|
516
|
+
el.scrollTop = rowBottom - clientHeight;
|
|
517
|
+
}
|
|
1144
518
|
}
|
|
1145
|
-
}
|
|
1146
519
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
columnMetrics: {
|
|
1164
|
-
leftPinnedWidth: leftWidth,
|
|
1165
|
-
rightPinnedWidth: rightWidth,
|
|
1166
|
-
unpinnedAccWidths,
|
|
1167
|
-
unpinnedCols: unpinnedColsWithWidth,
|
|
1168
|
-
totalSpecialsWidth: systemColumnsWidth,
|
|
1169
|
-
pinnedSpecialsWidth: (rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0) + (checkboxSelection && pinCheckboxColumn ? 48 : 0)
|
|
520
|
+
if (colIndex !== undefined && colIndex >= 0) {
|
|
521
|
+
// colIndex is an index into all data columns: leftPinned + unpinned + rightPinned
|
|
522
|
+
const allDataCols = [...layout.leftPinnedCols, ...layout.unpinnedColsWithWidth, ...layout.rightPinnedCols];
|
|
523
|
+
const targetCol = allDataCols[colIndex];
|
|
524
|
+
if (!targetCol) return;
|
|
525
|
+
const unpinnedIndex = layout.unpinnedColsWithWidth.findIndex(c => c.field === targetCol.field);
|
|
526
|
+
if (unpinnedIndex === -1) return; // pinned column — always visible, no scroll needed
|
|
527
|
+
const colLocalLeft = unpinnedIndex > 0 ? layout.unpinnedAccWidths[unpinnedIndex - 1] : 0;
|
|
528
|
+
const colLocalRight = layout.unpinnedAccWidths[unpinnedIndex] ?? colLocalLeft;
|
|
529
|
+
const colRight = layout.leftWidth + colLocalRight;
|
|
530
|
+
const { scrollLeft, clientWidth } = el;
|
|
531
|
+
if (colLocalLeft < scrollLeft) {
|
|
532
|
+
el.scrollLeft = colLocalLeft;
|
|
533
|
+
} else if (colRight > scrollLeft + clientWidth) {
|
|
534
|
+
el.scrollLeft = colRight - clientWidth;
|
|
535
|
+
}
|
|
1170
536
|
}
|
|
1171
537
|
};
|
|
1172
|
-
}, [
|
|
1173
|
-
layout,
|
|
1174
|
-
state.virtualization.scrollTop,
|
|
1175
|
-
state.virtualization.scrollLeft,
|
|
1176
|
-
state.dimensions.viewportHeight,
|
|
1177
|
-
state.dimensions.viewportWidth,
|
|
1178
|
-
autoHeight
|
|
1179
|
-
]);
|
|
1180
|
-
|
|
1181
|
-
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
|
1182
|
-
const target = event.currentTarget;
|
|
1183
|
-
setScroll(target.scrollTop, target.scrollLeft);
|
|
1184
|
-
|
|
1185
|
-
if (onRowsScrollEnd) {
|
|
1186
|
-
const { scrollTop, scrollHeight, clientHeight } = target;
|
|
1187
|
-
const scrollThreshold = 100;
|
|
538
|
+
}, [layout, viewportRef, gridData.apiRef]);
|
|
1188
539
|
|
|
1189
|
-
|
|
540
|
+
const virtualization = useGridVirtualization({
|
|
541
|
+
layout,
|
|
542
|
+
scrollTop,
|
|
543
|
+
scrollLeft,
|
|
544
|
+
viewportWidth: state.dimensions.viewportWidth,
|
|
545
|
+
viewportHeight: state.dimensions.viewportHeight,
|
|
546
|
+
autoHeight,
|
|
547
|
+
rowReordering,
|
|
548
|
+
hasDetailPanel,
|
|
549
|
+
checkboxSelection,
|
|
550
|
+
pinCheckboxColumn,
|
|
551
|
+
pinExpandColumn,
|
|
552
|
+
});
|
|
1190
553
|
|
|
1191
|
-
onRowsScrollEnd({
|
|
1192
|
-
visibleTop: scrollTop,
|
|
1193
|
-
visibleBottom: scrollTop + clientHeight,
|
|
1194
|
-
viewportHeight: clientHeight
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}, [setScroll, onRowsScrollEnd]);
|
|
1199
554
|
|
|
1200
555
|
useEffect(() => {
|
|
1201
556
|
if (!viewportRef.current) return;
|
|
@@ -1227,28 +582,46 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1227
582
|
setInternalRowSelectionModel(newSelectionArray);
|
|
1228
583
|
}
|
|
1229
584
|
onRowSelectionModelChange?.(newSelectionArray);
|
|
1230
|
-
}, [selectedRowIds, isSelectionControlled, onRowSelectionModelChange]);
|
|
585
|
+
}, [selectedRowIds, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
|
|
1231
586
|
|
|
1232
587
|
const handleSelectAll = useCallback((isSelected: boolean) => {
|
|
1233
588
|
let newSelection: GridRowId[] = [];
|
|
1234
589
|
if (isSelected) {
|
|
1235
|
-
newSelection =
|
|
590
|
+
newSelection = effectiveRows.map((row: GridRowModel) => row.id);
|
|
1236
591
|
}
|
|
1237
592
|
|
|
1238
593
|
if (!isSelectionControlled) {
|
|
1239
594
|
setInternalRowSelectionModel(newSelection);
|
|
1240
595
|
}
|
|
1241
596
|
onRowSelectionModelChange?.(newSelection);
|
|
1242
|
-
}, [
|
|
597
|
+
}, [effectiveRows, isSelectionControlled, onRowSelectionModelChange, setInternalRowSelectionModel]);
|
|
1243
598
|
|
|
1244
599
|
const handleSort = useCallback((field: string, direction: GridSortDirection) => {
|
|
1245
600
|
const newSortModel = direction ? [{ field, sort: direction }] : [];
|
|
1246
601
|
|
|
1247
602
|
if (!isSortControlled) {
|
|
1248
|
-
setInternalSortModel(newSortModel as
|
|
603
|
+
setInternalSortModel(newSortModel as GridSortItem[]);
|
|
1249
604
|
}
|
|
1250
|
-
onSortModelChange?.(newSortModel as
|
|
1251
|
-
}, [isSortControlled, onSortModelChange]);
|
|
605
|
+
onSortModelChange?.(newSortModel as GridSortItem[]);
|
|
606
|
+
}, [isSortControlled, onSortModelChange, setInternalSortModel]);
|
|
607
|
+
|
|
608
|
+
const { focusedCell, setFocusedCell, handleFocus, handleBlur, handleKeyDown } = useGridKeyboardNavigation({
|
|
609
|
+
allRenderableRows,
|
|
610
|
+
navigationColumns,
|
|
611
|
+
checkboxSelection,
|
|
612
|
+
selectedRowIds,
|
|
613
|
+
handleSelectionChange,
|
|
614
|
+
handleDetailPanelToggle,
|
|
615
|
+
editingHandlers,
|
|
616
|
+
setKeyboardMode,
|
|
617
|
+
sortModel,
|
|
618
|
+
handleSort,
|
|
619
|
+
isCellEditable,
|
|
620
|
+
pagination,
|
|
621
|
+
pageSize: effectivePaginationModel.pageSize,
|
|
622
|
+
virtualization,
|
|
623
|
+
viewportRef,
|
|
624
|
+
});
|
|
1252
625
|
|
|
1253
626
|
const handleCellClick = useCallback((params: GridCellParams<R>) => {
|
|
1254
627
|
setKeyboardMode(false);
|
|
@@ -1256,7 +629,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1256
629
|
|
|
1257
630
|
gridRef.current?.focus({ preventScroll: true });
|
|
1258
631
|
onCellClick?.(params);
|
|
1259
|
-
}, [onCellClick, setKeyboardMode]);
|
|
632
|
+
}, [onCellClick, setKeyboardMode, setFocusedCell]);
|
|
1260
633
|
|
|
1261
634
|
const prevEditingCellRef = useRef(editingHandlers.editingCell);
|
|
1262
635
|
useEffect(() => {
|
|
@@ -1271,376 +644,68 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1271
644
|
prevEditingCellRef.current = editingHandlers.editingCell;
|
|
1272
645
|
}, [editingHandlers.editingCell]);
|
|
1273
646
|
|
|
1274
|
-
const visibleRows =
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
rowIndex: topPinnedCount + (pagination ? paginatedUnpinnedRows.length : sortedUnpinnedRows.length) + index
|
|
1283
|
-
}));
|
|
1284
|
-
|
|
1285
|
-
const centerStartIndex = Math.max(0, firstRowIndex - topPinnedCount);
|
|
1286
|
-
const centerEndIndex = Math.min(
|
|
1287
|
-
pagination ? paginatedUnpinnedRows.length : sortedUnpinnedRows.length,
|
|
1288
|
-
lastRowIndex - topPinnedCount + 1
|
|
1289
|
-
);
|
|
1290
|
-
|
|
1291
|
-
const centerRows = (pagination ? paginatedUnpinnedRows : sortedUnpinnedRows)
|
|
1292
|
-
.slice(centerStartIndex, centerEndIndex)
|
|
1293
|
-
.map((row, index) => ({
|
|
1294
|
-
row,
|
|
1295
|
-
rowIndex: topPinnedCount + centerStartIndex + index
|
|
1296
|
-
}));
|
|
1297
|
-
|
|
1298
|
-
const combined = [...topPinned, ...centerRows, ...bottomPinned];
|
|
1299
|
-
|
|
1300
|
-
const seenIds = new Set<GridRowId>();
|
|
1301
|
-
const deduplicated = combined.filter(item => {
|
|
1302
|
-
if (seenIds.has(item.row.id)) {
|
|
1303
|
-
return false;
|
|
1304
|
-
}
|
|
1305
|
-
seenIds.add(item.row.id);
|
|
1306
|
-
return true;
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
return deduplicated;
|
|
1310
|
-
}, [virtualization.renderContext, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination,
|
|
1311
|
-
paginationMode, state.dataSource.loading, effectivePaginationModel.pageSize, rowHeight]);
|
|
647
|
+
const visibleRows = useGridVisibleRows<R>({
|
|
648
|
+
renderContext: virtualization.renderContext,
|
|
649
|
+
pinnedTopRows,
|
|
650
|
+
pinnedBottomRows,
|
|
651
|
+
paginatedUnpinnedRows,
|
|
652
|
+
sortedUnpinnedRows,
|
|
653
|
+
pagination,
|
|
654
|
+
});
|
|
1312
655
|
|
|
1313
656
|
|
|
1314
657
|
const allSelected = rows.length > 0 && selectedRowIds.size === rows.length;
|
|
1315
658
|
const someSelected = selectedRowIds.size > 0 && selectedRowIds.size < rows.length;
|
|
1316
659
|
|
|
1317
|
-
const handleFocus = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
1318
|
-
|
|
1319
|
-
if (event.target === event.currentTarget) {
|
|
1320
|
-
setFocusedCell(prev => {
|
|
1321
|
-
if (prev) return prev;
|
|
1322
|
-
|
|
1323
|
-
if (checkboxSelection) {
|
|
1324
|
-
return { id: 'HEADER', field: '__checkbox_col__' };
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const firstRow = allRenderableRows[0];
|
|
1328
|
-
const firstCol = navigationColumns[0];
|
|
1329
|
-
if (firstRow && firstCol) {
|
|
1330
|
-
return { id: firstRow.id, field: firstCol.field };
|
|
1331
|
-
}
|
|
1332
|
-
return null;
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
}, [allRenderableRows, navigationColumns]);
|
|
1336
|
-
|
|
1337
|
-
const handleBlur = useCallback((event: React.FocusEvent<HTMLDivElement>) => {
|
|
1338
|
-
|
|
1339
|
-
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
|
|
1340
|
-
setFocusedCell(null);
|
|
1341
|
-
}
|
|
1342
|
-
}, []);
|
|
1343
|
-
|
|
1344
|
-
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
|
1345
|
-
setKeyboardMode(true);
|
|
1346
|
-
|
|
1347
|
-
if (!focusedCell) return;
|
|
1348
|
-
|
|
1349
|
-
const { id, field } = focusedCell;
|
|
1350
|
-
|
|
1351
|
-
const isEditing = Boolean(editingHandlers.editingCell);
|
|
1352
|
-
|
|
1353
|
-
if (event.key === 'Enter') {
|
|
1354
|
-
event.preventDefault();
|
|
1355
|
-
if (isEditing) {
|
|
1356
|
-
editingHandlers.stopCellEdit();
|
|
1357
|
-
} else {
|
|
1358
|
-
|
|
1359
|
-
const col = navigationColumns.find(c => c.field === field);
|
|
1360
|
-
if (col?.editable) {
|
|
1361
|
-
const row = allRenderableRows.find(r => r.id === id);
|
|
1362
|
-
if (row) {
|
|
1363
|
-
editingHandlers.startCellEdit({
|
|
1364
|
-
id,
|
|
1365
|
-
field,
|
|
1366
|
-
value: (row as any)[field]
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
if (event.key === ' ' || event.key === 'Spacebar') {
|
|
1375
|
-
if (!isEditing) {
|
|
1376
|
-
if (field === '__checkbox_col__') {
|
|
1377
|
-
event.preventDefault();
|
|
1378
|
-
handleSelectionChange(id, !selectedRowIds.has(id));
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (field === '__expand_col__') {
|
|
1382
|
-
event.preventDefault();
|
|
1383
|
-
handleDetailPanelToggle(id);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (id === 'HEADER' && (event.key === 'Enter' || event.key === ' ')) {
|
|
1390
|
-
event.preventDefault();
|
|
1391
|
-
|
|
1392
|
-
const col = navigationColumns.find(c => c.field === field);
|
|
1393
|
-
if (col && (col as any).sortable !== false) {
|
|
1394
|
-
const currentSort = sortModel.find(item => item.field === field);
|
|
1395
|
-
let newDirection: GridSortDirection = 'asc';
|
|
1396
|
-
|
|
1397
|
-
if (currentSort) {
|
|
1398
|
-
if (currentSort.sort === 'asc') {
|
|
1399
|
-
newDirection = 'desc';
|
|
1400
|
-
} else {
|
|
1401
|
-
newDirection = null;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
handleSort(field, newDirection);
|
|
1405
|
-
}
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (event.key === 'Escape') {
|
|
1410
|
-
if (isEditing) {
|
|
1411
|
-
event.preventDefault();
|
|
1412
|
-
editingHandlers.stopCellEdit({ cancel: true });
|
|
1413
|
-
}
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (isEditing && event.key !== 'Tab') {
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const isHeader = id === 'HEADER';
|
|
1422
|
-
const rowIndex = isHeader ? -1 : allRenderableRows.findIndex(r => r.id === id);
|
|
1423
|
-
const colIndex = navigationColumns.findIndex(c => c.field === field);
|
|
1424
|
-
|
|
1425
|
-
if (!isHeader && rowIndex === -1) return;
|
|
1426
|
-
if (colIndex === -1) return;
|
|
1427
|
-
|
|
1428
|
-
// Find the next FOCUSABLE cell (any cell — editability not required for arrow navigation)
|
|
1429
|
-
const findNextCell = (
|
|
1430
|
-
startRow: number,
|
|
1431
|
-
startCol: number,
|
|
1432
|
-
deltaRow: number,
|
|
1433
|
-
deltaCol: number,
|
|
1434
|
-
wrapRow: boolean,
|
|
1435
|
-
allowHeader: boolean = false
|
|
1436
|
-
) => {
|
|
1437
|
-
let r = startRow + deltaRow;
|
|
1438
|
-
let c = startCol + deltaCol;
|
|
1439
|
-
|
|
1440
|
-
if (wrapRow && deltaCol !== 0) {
|
|
1441
|
-
if (deltaCol > 0 && c >= navigationColumns.length) { c = 0; r++; }
|
|
1442
|
-
else if (deltaCol < 0 && c < 0) { c = navigationColumns.length - 1; r--; }
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (!wrapRow && (c < 0 || c >= navigationColumns.length)) return null;
|
|
1446
|
-
if (r < (allowHeader ? -1 : 0) || r >= allRenderableRows.length) return null;
|
|
1447
|
-
|
|
1448
|
-
return { r, c };
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
// Find the next EDITABLE cell (for Tab key navigation)
|
|
1452
|
-
const findNextEditable = (
|
|
1453
|
-
startRow: number,
|
|
1454
|
-
startCol: number,
|
|
1455
|
-
deltaRow: number,
|
|
1456
|
-
deltaCol: number,
|
|
1457
|
-
wrapRow: boolean,
|
|
1458
|
-
allowHeader: boolean = false
|
|
1459
|
-
) => {
|
|
1460
|
-
let r = startRow;
|
|
1461
|
-
let c = startCol;
|
|
1462
|
-
let steps = 0;
|
|
1463
|
-
|
|
1464
|
-
const maxSteps = allRenderableRows.length * navigationColumns.length + navigationColumns.length;
|
|
1465
|
-
|
|
1466
|
-
while (steps < maxSteps) {
|
|
1467
|
-
steps++;
|
|
1468
|
-
|
|
1469
|
-
r += deltaRow;
|
|
1470
|
-
c += deltaCol;
|
|
1471
|
-
|
|
1472
|
-
if (wrapRow && deltaCol !== 0) {
|
|
1473
|
-
if (deltaCol > 0) {
|
|
1474
|
-
if (c >= navigationColumns.length) { c = 0; r++; }
|
|
1475
|
-
} else {
|
|
1476
|
-
if (c < 0) { c = navigationColumns.length - 1; r--; }
|
|
1477
|
-
}
|
|
1478
|
-
} else {
|
|
1479
|
-
if (c < 0 || c >= navigationColumns.length) return null;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
if (r < -1 || r >= allRenderableRows.length) return null;
|
|
1483
|
-
|
|
1484
|
-
if (r === -1) {
|
|
1485
|
-
if (!allowHeader) continue;
|
|
1486
|
-
const col = navigationColumns[c];
|
|
1487
|
-
const isSortable = (col as any).sortable !== false;
|
|
1488
|
-
const isInteractive = ['__checkbox_col__'].includes(col.field) || isSortable;
|
|
1489
|
-
if (isInteractive) return { r, c };
|
|
1490
|
-
continue;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
const row = allRenderableRows[r];
|
|
1494
|
-
const col = navigationColumns[c];
|
|
1495
|
-
|
|
1496
|
-
const isInteractable = ['__checkbox_col__', '__expand_col__', '__reorder_col__'].includes(col.field);
|
|
1497
|
-
let isEditable = col.editable || isInteractable;
|
|
1498
|
-
|
|
1499
|
-
if (isCellEditable && !isInteractable) {
|
|
1500
|
-
try {
|
|
1501
|
-
isEditable = isCellEditable({
|
|
1502
|
-
row,
|
|
1503
|
-
field: col.field,
|
|
1504
|
-
value: (row as any)[col.field],
|
|
1505
|
-
colDef: col,
|
|
1506
|
-
rowIndex: r,
|
|
1507
|
-
colIndex: c
|
|
1508
|
-
});
|
|
1509
|
-
} catch (e) {
|
|
1510
|
-
console.error('Error in isCellEditable:', e);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
if (isEditable) return { r, c };
|
|
1515
|
-
}
|
|
1516
|
-
return null;
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
|
-
let nextRowIndex = rowIndex;
|
|
1520
|
-
let nextColIndex = colIndex;
|
|
1521
|
-
let handled = false;
|
|
1522
|
-
|
|
1523
|
-
if (event.key === 'Tab') {
|
|
1524
|
-
// Tab moves to next EDITABLE cell
|
|
1525
|
-
const dir = event.shiftKey ? -1 : 1;
|
|
1526
|
-
const res = findNextEditable(rowIndex, colIndex, 0, dir, true, false);
|
|
1527
|
-
if (res) {
|
|
1528
|
-
nextRowIndex = res.r;
|
|
1529
|
-
nextColIndex = res.c;
|
|
1530
|
-
handled = true;
|
|
1531
|
-
}
|
|
1532
|
-
} else if (event.key === 'ArrowRight') {
|
|
1533
|
-
const res = findNextCell(rowIndex, colIndex, 0, 1, true, true);
|
|
1534
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1535
|
-
} else if (event.key === 'ArrowLeft') {
|
|
1536
|
-
const res = findNextCell(rowIndex, colIndex, 0, -1, true, true);
|
|
1537
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1538
|
-
} else if (event.key === 'ArrowDown') {
|
|
1539
|
-
const res = findNextCell(rowIndex, colIndex, 1, 0, false, true);
|
|
1540
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1541
|
-
} else if (event.key === 'ArrowUp') {
|
|
1542
|
-
const res = findNextCell(rowIndex, colIndex, -1, 0, false, true);
|
|
1543
|
-
if (res) { nextRowIndex = res.r; nextColIndex = res.c; handled = true; }
|
|
1544
|
-
} else if (event.key === 'Home') {
|
|
1545
|
-
if (event.ctrlKey || event.metaKey) {
|
|
1546
|
-
nextRowIndex = -1;
|
|
1547
|
-
nextColIndex = 0;
|
|
1548
|
-
} else {
|
|
1549
|
-
nextColIndex = 0;
|
|
1550
|
-
}
|
|
1551
|
-
handled = true;
|
|
1552
|
-
} else if (event.key === 'End') {
|
|
1553
|
-
if (event.ctrlKey || event.metaKey) {
|
|
1554
|
-
nextRowIndex = allRenderableRows.length - 1;
|
|
1555
|
-
nextColIndex = navigationColumns.length - 1;
|
|
1556
|
-
} else {
|
|
1557
|
-
nextColIndex = navigationColumns.length - 1;
|
|
1558
|
-
}
|
|
1559
|
-
handled = true;
|
|
1560
|
-
} else if (event.key === 'PageUp') {
|
|
1561
|
-
const pageSize = pagination ? effectivePaginationModel.pageSize : 10;
|
|
1562
|
-
nextRowIndex = Math.max(-1, nextRowIndex - pageSize);
|
|
1563
|
-
handled = true;
|
|
1564
|
-
} else if (event.key === 'PageDown') {
|
|
1565
|
-
const pageSize = pagination ? effectivePaginationModel.pageSize : 10;
|
|
1566
|
-
nextRowIndex = Math.min(allRenderableRows.length - 1, nextRowIndex + pageSize);
|
|
1567
|
-
handled = true;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
if (handled) {
|
|
1571
|
-
event.preventDefault();
|
|
1572
|
-
|
|
1573
|
-
if (isEditing && event.key === 'Tab') {
|
|
1574
|
-
editingHandlers.stopCellEdit();
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
if (nextRowIndex >= -1 && nextRowIndex < allRenderableRows.length &&
|
|
1578
|
-
nextColIndex >= 0 && nextColIndex < navigationColumns.length) {
|
|
1579
|
-
|
|
1580
|
-
const nextCol = navigationColumns[nextColIndex];
|
|
1581
|
-
|
|
1582
|
-
if (nextRowIndex === -1) {
|
|
1583
|
-
setFocusedCell({ id: 'HEADER', field: nextCol.field });
|
|
1584
|
-
} else {
|
|
1585
|
-
const nextRow = allRenderableRows[nextRowIndex];
|
|
1586
|
-
setFocusedCell({ id: nextRow.id, field: nextCol.field });
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
if (viewportRef.current) {
|
|
1590
|
-
const { cumulativeHeights, pinnedTopHeight } = virtualization;
|
|
1591
|
-
const { clientHeight, clientWidth, scrollTop, scrollLeft } = viewportRef.current;
|
|
1592
|
-
|
|
1593
|
-
const rowTop = nextRowIndex === 0 ? 0 : cumulativeHeights[nextRowIndex - 1];
|
|
1594
|
-
const rowBottom = cumulativeHeights[nextRowIndex];
|
|
1595
|
-
const pinnedOffset = pinnedTopHeight;
|
|
1596
|
-
const visibleTop = scrollTop + pinnedOffset;
|
|
1597
|
-
const visibleBottom = scrollTop + clientHeight;
|
|
1598
|
-
|
|
1599
|
-
let newScrollTop = scrollTop;
|
|
1600
|
-
if (rowTop < visibleTop) {
|
|
1601
|
-
newScrollTop = Math.max(0, rowTop - pinnedOffset);
|
|
1602
|
-
} else if (rowBottom > visibleBottom) {
|
|
1603
|
-
newScrollTop = rowBottom - clientHeight;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
if (newScrollTop !== scrollTop) {
|
|
1607
|
-
viewportRef.current.scrollTop = newScrollTop;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
if (virtualization.columnMetrics) {
|
|
1611
|
-
const { leftPinnedWidth, rightPinnedWidth, unpinnedAccWidths, unpinnedCols, totalSpecialsWidth, pinnedSpecialsWidth } = virtualization.columnMetrics;
|
|
1612
|
-
const field = nextCol.field;
|
|
1613
|
-
const unpinnedIndex = unpinnedCols.findIndex((c: any) => c.field === field);
|
|
1614
|
-
|
|
1615
|
-
if (unpinnedIndex !== -1) {
|
|
1616
|
-
const colLeft = totalSpecialsWidth + leftPinnedWidth + (unpinnedIndex > 0 ? unpinnedAccWidths[unpinnedIndex - 1] : 0);
|
|
1617
|
-
const colRight = totalSpecialsWidth + leftPinnedWidth + unpinnedAccWidths[unpinnedIndex];
|
|
1618
|
-
|
|
1619
|
-
const visibleStart = scrollLeft + pinnedSpecialsWidth + leftPinnedWidth;
|
|
1620
|
-
const visibleEnd = scrollLeft + clientWidth - rightPinnedWidth;
|
|
1621
|
-
|
|
1622
|
-
let newScrollLeft = scrollLeft;
|
|
1623
|
-
|
|
1624
|
-
if (colLeft < visibleStart) {
|
|
1625
|
-
newScrollLeft = Math.max(0, colLeft - pinnedSpecialsWidth - leftPinnedWidth);
|
|
1626
|
-
} else if (colRight > visibleEnd) {
|
|
1627
|
-
newScrollLeft = colRight - clientWidth + rightPinnedWidth;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
if (newScrollLeft !== scrollLeft) {
|
|
1631
|
-
viewportRef.current.scrollLeft = newScrollLeft;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}, [focusedCell, allRenderableRows, navigationColumns, editingHandlers, virtualization, selectedRowIds, handleSelectionChange, handleDetailPanelToggle]);
|
|
1639
|
-
|
|
1640
660
|
const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
|
|
1641
661
|
const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
|
|
1642
662
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
1643
663
|
|
|
664
|
+
const toolbarProps = React.useMemo(() => {
|
|
665
|
+
if (!slots?.toolbar) return null;
|
|
666
|
+
const reorderHandler = disableColumnReorder
|
|
667
|
+
? undefined
|
|
668
|
+
: (fromField: string, toField: string) => {
|
|
669
|
+
const currentOrder = [...effectiveColumnOrder];
|
|
670
|
+
const fromIdx = currentOrder.indexOf(fromField);
|
|
671
|
+
const toIdx = currentOrder.indexOf(toField);
|
|
672
|
+
if (fromIdx === -1 || toIdx === -1) return;
|
|
673
|
+
const newOrder = [...currentOrder];
|
|
674
|
+
newOrder.splice(fromIdx, 1);
|
|
675
|
+
newOrder.splice(toIdx, 0, fromField);
|
|
676
|
+
if (!columnOrder) setInternalColumnOrder(newOrder);
|
|
677
|
+
const col = effectiveColumns.find(c => c.field === fromField);
|
|
678
|
+
if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as unknown as GridColDef });
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
apiRef: gridData.apiRef,
|
|
682
|
+
columns: orderedColumns as unknown as GridColDef[],
|
|
683
|
+
baseColumns: columns as unknown as GridColDef[],
|
|
684
|
+
aggregationModel,
|
|
685
|
+
onAggregationModelChange: handleAggregationModelChange,
|
|
686
|
+
...(pivotMode || propPivotModel || onPivotModelChange ? {
|
|
687
|
+
pivotModel: currentPivotModel,
|
|
688
|
+
onPivotModelChange: handlePivotModelChange,
|
|
689
|
+
} : {}),
|
|
690
|
+
filterModel,
|
|
691
|
+
onFilterModelChange,
|
|
692
|
+
columnVisibilityModel,
|
|
693
|
+
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
694
|
+
onColumnReorder: reorderHandler,
|
|
695
|
+
onColumnOrderReset: disableColumnReorder ? undefined : () => setInternalColumnOrder(columns.map(c => c.field)),
|
|
696
|
+
forceColumnsOpen: columnsPanelOpen,
|
|
697
|
+
onColumnsPanelClose: () => setColumnsPanelOpen(false),
|
|
698
|
+
...slotProps?.toolbar,
|
|
699
|
+
};
|
|
700
|
+
}, [
|
|
701
|
+
slots?.toolbar, disableColumnReorder, effectiveColumnOrder, orderedColumns, effectiveColumns,
|
|
702
|
+
columnOrder, onColumnOrderChange, setInternalColumnOrder, gridData.apiRef,
|
|
703
|
+
columns, aggregationModel, handleAggregationModelChange, pivotMode,
|
|
704
|
+
propPivotModel, onPivotModelChange, currentPivotModel, handlePivotModelChange,
|
|
705
|
+
filterModel, onFilterModelChange, columnVisibilityModel,
|
|
706
|
+
handleColumnVisibilityModelChange, columnsPanelOpen, slotProps?.toolbar,
|
|
707
|
+
]);
|
|
708
|
+
|
|
1644
709
|
// Click-outside handler for standalone column panel
|
|
1645
710
|
useEffect(() => {
|
|
1646
711
|
if (!columnsPanelOpen || slots?.toolbar) return;
|
|
@@ -1663,188 +728,49 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1663
728
|
height: height ?? style?.height,
|
|
1664
729
|
'--ogx-row-height': `${rowHeight}px`,
|
|
1665
730
|
'--ogx-header-height': `${headerHeight}px`
|
|
1666
|
-
} as
|
|
731
|
+
} as unknown as React.CSSProperties}
|
|
1667
732
|
aria-busy={effectiveLoading}
|
|
1668
733
|
>
|
|
1669
|
-
{ }
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
columnVisibilityModel,
|
|
1686
|
-
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
1687
|
-
|
|
1688
|
-
onColumnReorder: disableColumnReorder ? undefined : (fromField: string, toField: string) => {
|
|
1689
|
-
const currentOrder = [...effectiveColumnOrder];
|
|
1690
|
-
const fromIdx = currentOrder.indexOf(fromField);
|
|
1691
|
-
const toIdx = currentOrder.indexOf(toField);
|
|
1692
|
-
if (fromIdx === -1 || toIdx === -1) return;
|
|
1693
|
-
const newOrder = [...currentOrder];
|
|
1694
|
-
newOrder.splice(fromIdx, 1);
|
|
1695
|
-
newOrder.splice(toIdx, 0, fromField);
|
|
1696
|
-
if (!columnOrder) setInternalColumnOrder(newOrder);
|
|
1697
|
-
const col = effectiveColumns.find(c => c.field === fromField); if (col) onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: col as any });
|
|
1698
|
-
},
|
|
1699
|
-
|
|
1700
|
-
forceColumnsOpen: columnsPanelOpen,
|
|
1701
|
-
onColumnsPanelClose: () => setColumnsPanelOpen(false),
|
|
1702
|
-
|
|
1703
|
-
...slotProps?.toolbar,
|
|
1704
|
-
};
|
|
1705
|
-
return <StableToolbar {...toolbarProps} />;
|
|
1706
|
-
})()}
|
|
1707
|
-
|
|
1708
|
-
{/* Fallback standalone column panel when no toolbar is present */}
|
|
1709
|
-
{!slots?.toolbar && columnsPanelOpen && ReactDOM.createPortal(
|
|
1710
|
-
<div
|
|
1711
|
-
id="ogx-standalone-col-panel"
|
|
1712
|
-
style={(() => {
|
|
1713
|
-
const rect = containerRef.current?.getBoundingClientRect();
|
|
1714
|
-
return {
|
|
1715
|
-
position: 'fixed',
|
|
1716
|
-
top: rect ? rect.top + 8 : 16,
|
|
1717
|
-
right: rect ? window.innerWidth - rect.right + 8 : 16,
|
|
1718
|
-
zIndex: 9999,
|
|
1719
|
-
display: 'inline-block',
|
|
1720
|
-
};
|
|
1721
|
-
})()}
|
|
1722
|
-
>
|
|
1723
|
-
<button
|
|
1724
|
-
onClick={() => setColumnsPanelOpen(false)}
|
|
1725
|
-
style={{
|
|
1726
|
-
position: 'absolute',
|
|
1727
|
-
top: -12,
|
|
1728
|
-
right: -12,
|
|
1729
|
-
width: 24,
|
|
1730
|
-
height: 24,
|
|
1731
|
-
display: 'flex',
|
|
1732
|
-
alignItems: 'center',
|
|
1733
|
-
justifyContent: 'center',
|
|
1734
|
-
background: '#334155',
|
|
1735
|
-
color: '#fff',
|
|
1736
|
-
border: 'none',
|
|
1737
|
-
borderRadius: '50%',
|
|
1738
|
-
cursor: 'pointer',
|
|
1739
|
-
fontSize: 14,
|
|
1740
|
-
lineHeight: 1,
|
|
1741
|
-
zIndex: 1,
|
|
1742
|
-
boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
|
|
1743
|
-
}}
|
|
1744
|
-
aria-label="Close"
|
|
1745
|
-
>×</button>
|
|
1746
|
-
<ColumnVisibilityPanel
|
|
1747
|
-
columns={effectiveColumns}
|
|
1748
|
-
visibleColumns={new Set(
|
|
1749
|
-
effectiveColumns
|
|
1750
|
-
.filter(col => columnVisibilityModel[col.field] !== false)
|
|
1751
|
-
.map(col => col.field)
|
|
1752
|
-
)}
|
|
1753
|
-
onVisibilityChange={(field, isVisible) => {
|
|
1754
|
-
handleColumnVisibilityModelChange({ ...columnVisibilityModel, [field]: isVisible });
|
|
1755
|
-
}}
|
|
1756
|
-
onShowAll={() => {
|
|
1757
|
-
const next = { ...columnVisibilityModel };
|
|
1758
|
-
effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = true; });
|
|
1759
|
-
handleColumnVisibilityModelChange(next);
|
|
1760
|
-
}}
|
|
1761
|
-
onHideAll={() => {
|
|
1762
|
-
const next = { ...columnVisibilityModel };
|
|
1763
|
-
effectiveColumns.forEach(col => { if (col.hideable !== false) next[col.field] = false; });
|
|
1764
|
-
handleColumnVisibilityModelChange(next);
|
|
1765
|
-
}}
|
|
1766
|
-
onColumnReorder={disableColumnReorder ? undefined : (fromField, toField) => {
|
|
1767
|
-
const currentOrder = [...effectiveColumnOrder];
|
|
1768
|
-
const fromIdx = currentOrder.indexOf(fromField);
|
|
1769
|
-
const toIdx = currentOrder.indexOf(toField);
|
|
1770
|
-
if (fromIdx === -1 || toIdx === -1) return;
|
|
1771
|
-
const newOrder = [...currentOrder];
|
|
1772
|
-
newOrder.splice(fromIdx, 1);
|
|
1773
|
-
newOrder.splice(toIdx, 0, fromField);
|
|
1774
|
-
if (!columnOrder) {
|
|
1775
|
-
setInternalColumnOrder(newOrder);
|
|
1776
|
-
}
|
|
1777
|
-
onColumnOrderChange?.({ oldIndex: fromIdx, targetIndex: toIdx, column: effectiveColumns.find(c => c.field === fromField) as any });
|
|
1778
|
-
}}
|
|
1779
|
-
/>
|
|
1780
|
-
</div>,
|
|
1781
|
-
containerRef.current?.closest('.ogx-theme-provider') || document.body
|
|
734
|
+
{toolbarProps && <StableToolbar {...toolbarProps} />}
|
|
735
|
+
|
|
736
|
+
{!slots?.toolbar && (
|
|
737
|
+
<GridStandaloneColumnPanel<R>
|
|
738
|
+
isOpen={columnsPanelOpen}
|
|
739
|
+
containerRef={containerRef}
|
|
740
|
+
effectiveColumns={effectiveColumns}
|
|
741
|
+
columnVisibilityModel={columnVisibilityModel}
|
|
742
|
+
effectiveColumnOrder={effectiveColumnOrder}
|
|
743
|
+
columnOrder={columnOrder}
|
|
744
|
+
disableColumnReorder={disableColumnReorder}
|
|
745
|
+
onClose={() => setColumnsPanelOpen(false)}
|
|
746
|
+
onColumnVisibilityChange={handleColumnVisibilityModelChange}
|
|
747
|
+
onColumnOrderChange={onColumnOrderChange}
|
|
748
|
+
setInternalColumnOrder={setInternalColumnOrder}
|
|
749
|
+
/>
|
|
1782
750
|
)}
|
|
1783
751
|
|
|
1784
|
-
{/* ═══════════════════════════════════════════════════════════════
|
|
1785
|
-
LIST VIEW — single-column card layout
|
|
1786
|
-
═══════════════════════════════════════════════════════════════ */}
|
|
1787
752
|
{listView && listViewColumn && (
|
|
1788
|
-
<
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
{
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
{
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
<path d="M3 9h18M9 21V9" />
|
|
1810
|
-
</svg>
|
|
1811
|
-
{noRowsLabel}
|
|
1812
|
-
</div>
|
|
1813
|
-
) : (
|
|
1814
|
-
allRenderableRows.map((row, idx) => (
|
|
1815
|
-
<ListViewRow<R>
|
|
1816
|
-
key={row.id}
|
|
1817
|
-
row={row as R}
|
|
1818
|
-
rowIndex={idx}
|
|
1819
|
-
listViewColumn={listViewColumn as any}
|
|
1820
|
-
isSelected={selectedRowIds.has(row.id)}
|
|
1821
|
-
checkboxSelection={checkboxSelection}
|
|
1822
|
-
rowHeight={rowHeight}
|
|
1823
|
-
onRowClick={(r) => handleRowClick({ row: r, id: r.id, rowIndex: idx })}
|
|
1824
|
-
onSelectionChange={handleSelectionChange}
|
|
1825
|
-
/>
|
|
1826
|
-
))
|
|
1827
|
-
)}
|
|
1828
|
-
</div>
|
|
1829
|
-
|
|
1830
|
-
{/* Pagination in list view — use filteredRows.length as total since state.pagination.rowCount is only set for server-side */}
|
|
1831
|
-
{pagination && (() => {
|
|
1832
|
-
const PaginationComponent = slots?.pagination || Pagination;
|
|
1833
|
-
const totalRowCount = (paginationMode === 'server' && dataSource)
|
|
1834
|
-
? (state.pagination.rowCount || 0)
|
|
1835
|
-
: filteredRows.length;
|
|
1836
|
-
const paginationProps = {
|
|
1837
|
-
page: effectivePaginationModel.page,
|
|
1838
|
-
pageSize: effectivePaginationModel.pageSize,
|
|
1839
|
-
rowCount: totalRowCount,
|
|
1840
|
-
pageSizeOptions: pageSizeOptions,
|
|
1841
|
-
onPageChange: (newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage }),
|
|
1842
|
-
onPageSizeChange: (newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 }),
|
|
1843
|
-
...slotProps?.pagination
|
|
1844
|
-
};
|
|
1845
|
-
return <PaginationComponent {...paginationProps} />;
|
|
1846
|
-
})()}
|
|
1847
|
-
</div>
|
|
753
|
+
<GridListView<R>
|
|
754
|
+
ariaLabel={ariaLabel}
|
|
755
|
+
allRenderableRows={allRenderableRows}
|
|
756
|
+
filteredRows={filteredRows}
|
|
757
|
+
pagination={pagination}
|
|
758
|
+
effectivePaginationModel={effectivePaginationModel}
|
|
759
|
+
pageSizeOptions={pageSizeOptions}
|
|
760
|
+
selectedRowIds={selectedRowIds}
|
|
761
|
+
listViewColumn={listViewColumn}
|
|
762
|
+
noRowsLabel={noRowsLabel}
|
|
763
|
+
rowHeight={rowHeight}
|
|
764
|
+
checkboxSelection={checkboxSelection}
|
|
765
|
+
paginationMode={paginationMode}
|
|
766
|
+
dataSource={dataSource}
|
|
767
|
+
serverRowCount={state.pagination.rowCount || 0}
|
|
768
|
+
paginationSlot={slots?.pagination}
|
|
769
|
+
paginationSlotProps={slotProps?.pagination}
|
|
770
|
+
onRowClick={handleRowClick}
|
|
771
|
+
onSelectionChange={handleSelectionChange}
|
|
772
|
+
onPaginationModelChange={handlePaginationModelChange}
|
|
773
|
+
/>
|
|
1848
774
|
)}
|
|
1849
775
|
|
|
1850
776
|
{/* ══════════════════════════════════════════════════════════════
|
|
@@ -1886,7 +812,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1886
812
|
{ }
|
|
1887
813
|
<Header
|
|
1888
814
|
columns={virtualization.virtualColumns}
|
|
1889
|
-
allColumns={effectiveColumns
|
|
815
|
+
allColumns={effectiveColumns}
|
|
1890
816
|
columnGroupingModel={columnGroupingModel}
|
|
1891
817
|
checkboxSelection={checkboxSelection}
|
|
1892
818
|
allSelected={allSelected}
|
|
@@ -1941,299 +867,135 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1941
867
|
|
|
1942
868
|
{/* Empty State Overlay (Standard View) — showing after header */}
|
|
1943
869
|
{!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
|
|
1944
|
-
<
|
|
1945
|
-
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
1946
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
1947
|
-
<path d="M3 9h18M9 21V9" />
|
|
1948
|
-
</svg>
|
|
1949
|
-
<span>{noRowsLabel}</span>
|
|
1950
|
-
</div>
|
|
870
|
+
<GridEmptyState noRowsLabel={noRowsLabel} width={virtualization.totalWidth} />
|
|
1951
871
|
)}
|
|
1952
872
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
pinExpandColumn={pinExpandColumn}
|
|
1978
|
-
|
|
1979
|
-
focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
|
|
1980
|
-
|
|
1981
|
-
colspanMap={spanning.colspanMap}
|
|
1982
|
-
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
1983
|
-
rowHeight={rowHeight}
|
|
1984
|
-
/>
|
|
1985
|
-
))}
|
|
1986
|
-
</div>
|
|
1987
|
-
)}
|
|
873
|
+
<GridPinnedRows<R>
|
|
874
|
+
rows={pinnedTopRows}
|
|
875
|
+
position="top"
|
|
876
|
+
columns={virtualization.virtualColumns}
|
|
877
|
+
selectedRowIds={selectedRowIds}
|
|
878
|
+
checkboxSelection={checkboxSelection}
|
|
879
|
+
onRowClick={handleRowClick}
|
|
880
|
+
onCellClick={handleCellClick}
|
|
881
|
+
onSelectionChange={handleSelectionChange}
|
|
882
|
+
columnWidths={columnWidths}
|
|
883
|
+
pinnedColumns={pinnedColumns}
|
|
884
|
+
pinnedRows={pinnedRows}
|
|
885
|
+
hasDetailPanel={hasDetailPanel}
|
|
886
|
+
expandedRowIds={expandedRowIds}
|
|
887
|
+
getDetailPanelContent={getDetailPanelContent}
|
|
888
|
+
getDetailPanelHeight={getDetailPanelHeight}
|
|
889
|
+
onDetailPanelToggle={handleDetailPanelToggle}
|
|
890
|
+
pinCheckboxColumn={pinCheckboxColumn}
|
|
891
|
+
pinExpandColumn={pinExpandColumn}
|
|
892
|
+
focusedCell={focusedCell}
|
|
893
|
+
colspanMap={spanning.colspanMap}
|
|
894
|
+
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
895
|
+
rowHeight={rowHeight}
|
|
896
|
+
/>
|
|
1988
897
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
{
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
row={row}
|
|
2051
|
-
columns={virtualization.virtualColumns}
|
|
2052
|
-
rowIndex={actualIndex}
|
|
2053
|
-
isSelected={selectedRowIds.has(row.id)}
|
|
2054
|
-
checkboxSelection={checkboxSelection}
|
|
2055
|
-
onRowClick={handleRowClick}
|
|
2056
|
-
onCellClick={handleCellClick}
|
|
2057
|
-
onSelectionChange={handleSelectionChange}
|
|
2058
|
-
columnWidths={columnWidths}
|
|
2059
|
-
pinnedColumns={pinnedColumns}
|
|
2060
|
-
pinnedRows={pinnedRows}
|
|
2061
|
-
// Detail Panel
|
|
2062
|
-
hasDetailPanel={hasDetailPanel}
|
|
2063
|
-
isDetailPanelExpanded={expandedRowIds.has(row.id)}
|
|
2064
|
-
detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: actualIndex }) : null}
|
|
2065
|
-
detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: actualIndex }) || 200}
|
|
2066
|
-
onDetailPanelToggle={handleDetailPanelToggle}
|
|
2067
|
-
pinCheckboxColumn={pinCheckboxColumn}
|
|
2068
|
-
pinExpandColumn={pinExpandColumn}
|
|
2069
|
-
// Row Reordering
|
|
2070
|
-
rowReordering={rowReordering}
|
|
2071
|
-
onDragStart={rowReorderHandlers.onDragStart}
|
|
2072
|
-
onDragOver={rowReorderHandlers.onDragOver}
|
|
2073
|
-
onDragEnd={rowReorderHandlers.onDragEnd}
|
|
2074
|
-
onDrop={rowReorderHandlers.onDrop}
|
|
2075
|
-
isDragging={rowReorderHandlers.draggedRowId === row.id}
|
|
2076
|
-
isDragOver={rowReorderHandlers.dragOverRowId === row.id}
|
|
2077
|
-
// Editing
|
|
2078
|
-
editingCell={editingHandlers.editingCell}
|
|
2079
|
-
onEditStart={editingHandlers.startCellEdit}
|
|
2080
|
-
onEditStop={editingHandlers.stopCellEdit}
|
|
2081
|
-
onEditCellValueChange={editingHandlers.setEditCellValue}
|
|
2082
|
-
// Focus
|
|
2083
|
-
focusedCellField={focusedCell != null && focusedCell.id === row.id ? focusedCell.field : null}
|
|
2084
|
-
// Spanning
|
|
2085
|
-
colspanMap={spanning.colspanMap}
|
|
2086
|
-
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
2087
|
-
rowHeight={rowHeight}
|
|
2088
|
-
/>
|
|
2089
|
-
);
|
|
2090
|
-
})
|
|
2091
|
-
)}
|
|
2092
|
-
|
|
2093
|
-
{/* Infinite Scroll Skeletons (Inline with flow, inside translateY container) */}
|
|
2094
|
-
{paginationMode === 'infinite' && state.dataSource.loading && sortedUnpinnedRows.length > 0 && (() => {
|
|
2095
|
-
const skeletonCount = Math.min(effectivePaginationModel.pageSize, 20);
|
|
2096
|
-
// The virtual container translates its children down by `firstRowIndex * rowHeight`.
|
|
2097
|
-
// Since these skeletons are at the VERY BOTTOM of all rows, their actual array index
|
|
2098
|
-
// is precisely after the last real row.
|
|
2099
|
-
const startIdx = layout.unpinnedRowsLength;
|
|
2100
|
-
|
|
2101
|
-
// However, they are rendered INSIDE the translateY block. The real rows handled this nicely
|
|
2102
|
-
// because they just layout one after another. Skeletons can do exactly the same, EXCEPT we might
|
|
2103
|
-
// need to offset them so they sit at the bottom.
|
|
2104
|
-
return (
|
|
2105
|
-
<div className="ogx__skeleton-group">
|
|
2106
|
-
{Array.from({ length: skeletonCount }).map((_, i) => (
|
|
2107
|
-
<Row
|
|
2108
|
-
key={`__skeleton_${i}__`}
|
|
2109
|
-
row={{ id: `__skeleton_${i}__`, _isSkeleton: true } as any}
|
|
2110
|
-
columns={virtualization.virtualColumns}
|
|
2111
|
-
rowIndex={startIdx + i}
|
|
2112
|
-
rowHeight={rowHeight}
|
|
2113
|
-
/>
|
|
2114
|
-
))}
|
|
2115
|
-
</div>
|
|
2116
|
-
);
|
|
2117
|
-
})()}
|
|
2118
|
-
</div>
|
|
2119
|
-
|
|
2120
|
-
</div>
|
|
2121
|
-
|
|
2122
|
-
{/* Pinned Bottom Rows */}
|
|
2123
|
-
{pinnedBottomRows.length > 0 && (
|
|
2124
|
-
<div className="ogx__pinned-rows ogx__pinned-rows--bottom" role="rowgroup">
|
|
2125
|
-
{pinnedBottomRows.map((row, index) => (
|
|
2126
|
-
<Row
|
|
2127
|
-
key={row.id}
|
|
2128
|
-
row={row}
|
|
2129
|
-
columns={virtualization.virtualColumns}
|
|
2130
|
-
rowIndex={index}
|
|
2131
|
-
isSelected={selectedRowIds.has(row.id)}
|
|
2132
|
-
checkboxSelection={checkboxSelection}
|
|
2133
|
-
onRowClick={handleRowClick}
|
|
2134
|
-
onCellClick={handleCellClick}
|
|
2135
|
-
onSelectionChange={handleSelectionChange}
|
|
2136
|
-
columnWidths={columnWidths}
|
|
2137
|
-
pinnedColumns={pinnedColumns}
|
|
2138
|
-
pinnedRows={pinnedRows}
|
|
2139
|
-
// Detail Panel
|
|
2140
|
-
hasDetailPanel={hasDetailPanel}
|
|
2141
|
-
isDetailPanelExpanded={expandedRowIds.has(row.id)}
|
|
2142
|
-
detailPanelContent={getDetailPanelContent ? getDetailPanelContent({ row, id: row.id, rowIndex: index }) : null}
|
|
2143
|
-
detailPanelHeight={getDetailPanelHeight?.({ row, id: row.id, rowIndex: index }) || 200}
|
|
2144
|
-
onDetailPanelToggle={handleDetailPanelToggle}
|
|
2145
|
-
pinCheckboxColumn={pinCheckboxColumn}
|
|
2146
|
-
pinExpandColumn={pinExpandColumn}
|
|
2147
|
-
// Focus
|
|
2148
|
-
focusedCellField={focusedCell?.id === row.id ? focusedCell.field : null}
|
|
2149
|
-
// Spanning
|
|
2150
|
-
colspanMap={spanning.colspanMap}
|
|
2151
|
-
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
2152
|
-
rowHeight={rowHeight}
|
|
2153
|
-
/>
|
|
2154
|
-
))}
|
|
2155
|
-
</div>
|
|
2156
|
-
)}
|
|
898
|
+
<GridVirtualRows<R>
|
|
899
|
+
virtualContainerHeight={virtualization.totalHeight - virtualization.pinnedTopHeight - virtualization.pinnedBottomHeight}
|
|
900
|
+
offsetTop={virtualization.offsetTop}
|
|
901
|
+
effectiveLoading={effectiveLoading}
|
|
902
|
+
visibleRows={visibleRows}
|
|
903
|
+
baseColumns={columns as unknown as GridColDef<R>[]}
|
|
904
|
+
virtualColumns={virtualization.virtualColumns}
|
|
905
|
+
viewportWidth={state.dimensions.viewportWidth}
|
|
906
|
+
checkboxSelection={checkboxSelection}
|
|
907
|
+
hasDetailPanel={hasDetailPanel}
|
|
908
|
+
rowReordering={rowReordering}
|
|
909
|
+
rowHeight={rowHeight}
|
|
910
|
+
pinnedRows={pinnedRows}
|
|
911
|
+
selectedRowIds={selectedRowIds}
|
|
912
|
+
onRowClick={handleRowClick}
|
|
913
|
+
onCellClick={handleCellClick}
|
|
914
|
+
onSelectionChange={handleSelectionChange}
|
|
915
|
+
columnWidths={columnWidths}
|
|
916
|
+
pinnedColumns={pinnedColumns}
|
|
917
|
+
expandedRowIds={expandedRowIds}
|
|
918
|
+
getDetailPanelContent={getDetailPanelContent}
|
|
919
|
+
getDetailPanelHeight={getDetailPanelHeight}
|
|
920
|
+
onDetailPanelToggle={handleDetailPanelToggle}
|
|
921
|
+
pinCheckboxColumn={pinCheckboxColumn}
|
|
922
|
+
pinExpandColumn={pinExpandColumn}
|
|
923
|
+
rowReorderHandlers={rowReorderHandlers}
|
|
924
|
+
editingHandlers={editingHandlers}
|
|
925
|
+
focusedCell={focusedCell}
|
|
926
|
+
colspanMap={spanning.colspanMap}
|
|
927
|
+
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
928
|
+
paginationMode={paginationMode}
|
|
929
|
+
dataSourceLoading={state.dataSource.loading}
|
|
930
|
+
sortedUnpinnedRowCount={sortedUnpinnedRows.length}
|
|
931
|
+
infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
|
|
932
|
+
unpinnedRowsLength={layout.unpinnedRowsLength}
|
|
933
|
+
/>
|
|
934
|
+
|
|
935
|
+
<GridPinnedRows<R>
|
|
936
|
+
rows={pinnedBottomRows}
|
|
937
|
+
position="bottom"
|
|
938
|
+
columns={virtualization.virtualColumns}
|
|
939
|
+
selectedRowIds={selectedRowIds}
|
|
940
|
+
checkboxSelection={checkboxSelection}
|
|
941
|
+
onRowClick={handleRowClick}
|
|
942
|
+
onCellClick={handleCellClick}
|
|
943
|
+
onSelectionChange={handleSelectionChange}
|
|
944
|
+
columnWidths={columnWidths}
|
|
945
|
+
pinnedColumns={pinnedColumns}
|
|
946
|
+
pinnedRows={pinnedRows}
|
|
947
|
+
hasDetailPanel={hasDetailPanel}
|
|
948
|
+
expandedRowIds={expandedRowIds}
|
|
949
|
+
getDetailPanelContent={getDetailPanelContent}
|
|
950
|
+
getDetailPanelHeight={getDetailPanelHeight}
|
|
951
|
+
onDetailPanelToggle={handleDetailPanelToggle}
|
|
952
|
+
pinCheckboxColumn={pinCheckboxColumn}
|
|
953
|
+
pinExpandColumn={pinExpandColumn}
|
|
954
|
+
focusedCell={focusedCell}
|
|
955
|
+
colspanMap={spanning.colspanMap}
|
|
956
|
+
rowSpanningCaches={spanning.rowSpanningState.caches}
|
|
957
|
+
rowHeight={rowHeight}
|
|
958
|
+
/>
|
|
2157
959
|
|
|
2158
960
|
{/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
|
|
2159
961
|
already contain pre-aggregated values with synthetic field keys
|
|
2160
962
|
(e.g. 'Q1\u001frevenue\u001fsum') that don't match aggregationModel keys. */}
|
|
2161
963
|
{hasAggregation && !pivotMode && (
|
|
2162
|
-
<
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
<div style={{ width: 48, flexShrink: 0 }} />
|
|
2173
|
-
)}
|
|
2174
|
-
{hasDetailPanel && (
|
|
2175
|
-
<div style={{ width: 48, flexShrink: 0 }} />
|
|
2176
|
-
)}
|
|
2177
|
-
{rowReordering && (
|
|
2178
|
-
<div style={{ width: 48, flexShrink: 0 }} />
|
|
2179
|
-
)}
|
|
2180
|
-
{orderedColumns.map((col) => {
|
|
2181
|
-
const fnName = (aggregationModel as Record<string, string>)[col.field];
|
|
2182
|
-
const rawValue = aggregationResult[col.field];
|
|
2183
|
-
const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
|
|
2184
|
-
|
|
2185
|
-
return (
|
|
2186
|
-
<div
|
|
2187
|
-
key={col.field}
|
|
2188
|
-
className="ogx__aggregation-cell"
|
|
2189
|
-
role="gridcell"
|
|
2190
|
-
style={{
|
|
2191
|
-
width: colWidth,
|
|
2192
|
-
minWidth: colWidth,
|
|
2193
|
-
maxWidth: colWidth,
|
|
2194
|
-
textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
|
|
2195
|
-
}}
|
|
2196
|
-
>
|
|
2197
|
-
{fnName ? (
|
|
2198
|
-
<>
|
|
2199
|
-
<span className="ogx__aggregation-label">
|
|
2200
|
-
{fnName}
|
|
2201
|
-
</span>
|
|
2202
|
-
<span className="ogx__aggregation-value">
|
|
2203
|
-
{formatAggregationValue(rawValue, fnName)}
|
|
2204
|
-
</span>
|
|
2205
|
-
</>
|
|
2206
|
-
) : null}
|
|
2207
|
-
</div>
|
|
2208
|
-
);
|
|
2209
|
-
})}
|
|
2210
|
-
</div>
|
|
964
|
+
<GridAggregationFooter
|
|
965
|
+
columns={orderedColumns as unknown as GridColDef[]}
|
|
966
|
+
aggregationModel={aggregationModel}
|
|
967
|
+
aggregationResult={aggregationResult}
|
|
968
|
+
columnWidths={columnWidths}
|
|
969
|
+
rowHeight={rowHeight}
|
|
970
|
+
checkboxSelection={checkboxSelection}
|
|
971
|
+
hasDetailPanel={hasDetailPanel}
|
|
972
|
+
rowReordering={rowReordering}
|
|
973
|
+
/>
|
|
2211
974
|
)}
|
|
2212
975
|
|
|
2213
976
|
</div>
|
|
2214
977
|
</div>
|
|
2215
978
|
)}
|
|
2216
979
|
|
|
2217
|
-
{/* Pagination — shared between grid and list view when not inside list view block */}
|
|
2218
980
|
{!listView && pagination && (() => {
|
|
2219
981
|
const PaginationComponent = slots?.pagination || Pagination;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
982
|
+
return (
|
|
983
|
+
<PaginationComponent
|
|
984
|
+
page={effectivePaginationModel.page}
|
|
985
|
+
pageSize={effectivePaginationModel.pageSize}
|
|
986
|
+
rowCount={state.pagination.rowCount}
|
|
987
|
+
pageSizeOptions={pageSizeOptions}
|
|
988
|
+
onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
989
|
+
onPageSizeChange={(newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
|
|
990
|
+
{...slotProps?.pagination}
|
|
991
|
+
/>
|
|
992
|
+
);
|
|
2231
993
|
})()}
|
|
2232
994
|
|
|
2233
995
|
{/* Accessibility Live Region */}
|
|
2234
996
|
<div className="ogx-aria-live-status" role="status" aria-live="polite">
|
|
2235
997
|
{effectiveLoading ? 'Loading data...' : ''}
|
|
2236
|
-
{state.dataSource.error ? `Error: ${state.dataSource.error.message
|
|
998
|
+
{state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
|
|
2237
999
|
{!loading && !state.dataSource.error && (
|
|
2238
1000
|
filteredRows.length === 0
|
|
2239
1001
|
? noRowsLabel
|
|
@@ -2243,27 +1005,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
2243
1005
|
)}
|
|
2244
1006
|
</div>
|
|
2245
1007
|
|
|
2246
|
-
{state.dataSource.error
|
|
2247
|
-
<div className="ogx-error-overlay" aria-live="assertive" role="alert">
|
|
2248
|
-
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--ogx-color-error)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
2249
|
-
<circle cx="12" cy="12" r="10" />
|
|
2250
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
2251
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
2252
|
-
</svg>
|
|
2253
|
-
<div className="ogx-error-overlay__title">
|
|
2254
|
-
Oops! Something went wrong
|
|
2255
|
-
</div>
|
|
2256
|
-
<div className="ogx-error-overlay__message">
|
|
2257
|
-
{state.dataSource.error.message || 'An unexpected error occurred while loading the data.'}
|
|
2258
|
-
</div>
|
|
2259
|
-
<button
|
|
2260
|
-
onClick={() => window.location.reload()}
|
|
2261
|
-
className="ogx-button ogx-button--primary"
|
|
2262
|
-
>
|
|
2263
|
-
Retry
|
|
2264
|
-
</button>
|
|
2265
|
-
</div>
|
|
2266
|
-
)}
|
|
1008
|
+
<GridErrorOverlay error={state.dataSource.error} />
|
|
2267
1009
|
</div>
|
|
2268
1010
|
);
|
|
2269
1011
|
}
|