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