@opencorestack/opengridx 0.1.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +162 -19
  3. package/dist/index.d.ts +60 -41
  4. package/dist/opengridx.es.js +5045 -4549
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +11 -11
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +17 -7
  9. package/docs/README.md +14 -0
  10. package/docs/components/aggregation-footer.md +48 -0
  11. package/docs/components/datagrid.md +8 -3
  12. package/docs/components/empty-state.md +50 -0
  13. package/docs/components/error-overlay.md +40 -0
  14. package/docs/features/editing-reordering.md +11 -7
  15. package/docs/features/filtering.md +10 -8
  16. package/docs/features/selection.md +2 -1
  17. package/docs/features/sorting-pagination.md +3 -1
  18. package/docs/features/state-persistence.md +1 -2
  19. package/docs/roadmap.md +20 -1
  20. package/lib/components/Cell/Cell.tsx +9 -10
  21. package/lib/components/Cell/GridEditInputCell.tsx +4 -4
  22. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  23. package/lib/components/DataGrid/DataGrid.regression.test.tsx +132 -0
  24. package/lib/components/DataGrid/DataGrid.tsx +336 -1018
  25. package/lib/components/DataGrid/GridAggregationFooter.tsx +68 -0
  26. package/lib/components/DataGrid/GridEmptyState.tsx +16 -0
  27. package/lib/components/DataGrid/GridErrorOverlay.tsx +33 -0
  28. package/lib/components/DataGrid/GridListView.tsx +116 -0
  29. package/lib/components/DataGrid/GridPinnedRows.tsx +98 -0
  30. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +124 -0
  31. package/lib/components/DataGrid/GridVirtualRows.tsx +202 -0
  32. package/lib/components/FilterPanel/FilterPanel.tsx +18 -10
  33. package/lib/components/Header/ColumnMenu.tsx +2 -2
  34. package/lib/components/Header/Header.tsx +4 -4
  35. package/lib/components/ListView/ListViewRow.tsx +2 -2
  36. package/lib/components/Row/Row.tsx +10 -10
  37. package/lib/components/SkeletonRow/SkeletonRow.tsx +9 -2
  38. package/lib/components/Toolbar/GridToolbar.tsx +6 -0
  39. package/lib/hooks/core/useDataGrid.regression.test.ts +66 -0
  40. package/lib/hooks/core/useDataGrid.ts +14 -34
  41. package/lib/hooks/core/useGridColumns.tsx +228 -0
  42. package/lib/hooks/core/useGridControlledState.ts +192 -0
  43. package/lib/hooks/core/useGridRowPipeline.ts +109 -0
  44. package/lib/hooks/core/useGridScrollSync.ts +52 -0
  45. package/lib/hooks/core/useGridStateSnapshot.ts +63 -0
  46. package/lib/hooks/core/useGridVirtualization.ts +186 -0
  47. package/lib/hooks/core/useGridVisibleRows.ts +66 -0
  48. package/lib/hooks/core/useLayout.ts +0 -6
  49. package/lib/hooks/features/useAggregation.ts +7 -7
  50. package/lib/hooks/features/useGridDataSource.ts +1 -1
  51. package/lib/hooks/features/useGridEditing.ts +31 -32
  52. package/lib/hooks/features/usePivot.test.ts +202 -0
  53. package/lib/hooks/features/usePivot.ts +0 -0
  54. package/lib/hooks/useColumnReorder.ts +1 -1
  55. package/lib/hooks/useRowGrouping.ts +14 -14
  56. package/lib/hooks/useTreeData.ts +1 -1
  57. package/lib/state/types.ts +1 -1
  58. package/lib/state/useGridStateStorage.ts +1 -1
  59. package/lib/types/index.ts +48 -32
  60. package/lib/utils/export/exportToExcelAdvanced.ts +14 -14
  61. package/lib/utils/export/index.ts +22 -17
  62. package/lib/utils/filtering/index.ts +20 -20
  63. package/lib/utils/sorting/index.ts +1 -1
  64. package/package.json +1 -1
@@ -0,0 +1,228 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
+ import { useColumnReorder } from '../useColumnReorder';
3
+ import { ExpandIcon } from '../../components/ui/ExpandIcon';
4
+ import type {
5
+ GridColDef,
6
+ GridRowModel,
7
+ GridRowId,
8
+ GridColumnOrderChangeParams,
9
+ GridRenderCellParams,
10
+ } from '../../types';
11
+ import type { GridInitialState } from '../../state/types';
12
+
13
+ interface HierarchyHandlers {
14
+ toggleExpansion: (id: GridRowId) => void;
15
+ }
16
+
17
+ export interface UseGridColumnsParams<R extends GridRowModel> {
18
+ activeColumns: GridColDef<R>[];
19
+ isHierarchyEnabled: boolean;
20
+ isRowGrouping: boolean;
21
+ isTreeData: boolean;
22
+ activeHierarchyHandlers: HierarchyHandlers | null;
23
+ columnVisibilityModel: Record<string, boolean>;
24
+ columnOrder?: string[];
25
+ onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
26
+ disableColumnReorder: boolean;
27
+ pivotMode: boolean;
28
+ checkboxSelection: boolean;
29
+ hasDetailPanel: boolean;
30
+ rowReordering: boolean;
31
+ initialState?: GridInitialState;
32
+ setColumns: (cols: GridColDef[]) => void;
33
+ }
34
+
35
+ export interface UseGridColumnsResult<R extends GridRowModel> {
36
+ effectiveColumns: GridColDef<R>[];
37
+ orderedColumns: GridColDef<R>[];
38
+ visibleOrderedColumns: GridColDef<R>[];
39
+ navigationColumns: Array<GridColDef<R> | { field: string }>;
40
+ columnWidths: Record<string, number>;
41
+ effectiveColumnOrder: string[];
42
+ setInternalColumnOrder: React.Dispatch<React.SetStateAction<string[]>>;
43
+ columnReorderHandlers: ReturnType<typeof useColumnReorder>;
44
+ handleColumnResize: (field: string, newWidth: number) => void;
45
+ }
46
+
47
+ export function useGridColumns<R extends GridRowModel>(
48
+ params: UseGridColumnsParams<R>
49
+ ): UseGridColumnsResult<R> {
50
+ const {
51
+ activeColumns,
52
+ isHierarchyEnabled,
53
+ isRowGrouping,
54
+ isTreeData,
55
+ activeHierarchyHandlers,
56
+ columnVisibilityModel,
57
+ columnOrder,
58
+ onColumnOrderChange,
59
+ disableColumnReorder,
60
+ pivotMode,
61
+ checkboxSelection,
62
+ hasDetailPanel,
63
+ rowReordering,
64
+ initialState,
65
+ setColumns,
66
+ } = params;
67
+
68
+ // ── Effective columns (hierarchy cell renderer injection) ─────────────────
69
+ const effectiveColumns = useMemo<GridColDef<R>[]>(() => {
70
+ if (!isHierarchyEnabled) return activeColumns;
71
+
72
+ return activeColumns.map((col, index) => {
73
+ if (index === 0) {
74
+ return {
75
+ ...col,
76
+ renderCell: (cellParams: GridRenderCellParams<R>) => {
77
+ const r = cellParams.row as Record<string, unknown>;
78
+ const depth = typeof r._treeDepth === 'number' ? r._treeDepth : 0;
79
+ const hasChildren = Boolean(r._hasChildren);
80
+ const isExpanded = Boolean(r._isExpanded);
81
+ const groupingField = typeof r._groupingField === 'string' ? r._groupingField : undefined;
82
+ const groupingValue = r._groupingValue;
83
+ const descendantCount = typeof r._descendantCount === 'number' ? r._descendantCount : undefined;
84
+ const isGroupRow = Boolean(r._isGroupRow);
85
+
86
+ let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : cellParams.value as React.ReactNode;
87
+
88
+ if (isTreeData && hasChildren && isGroupRow) {
89
+ content = (
90
+ <div className="ogx__group-cell-content">
91
+ {cellParams.value as React.ReactNode}
92
+ {descendantCount !== undefined && descendantCount > 0 ? ` (${descendantCount})` : ''}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ if (isRowGrouping && hasChildren && groupingField) {
98
+ content = (
99
+ <div className="ogx__group-cell-content">
100
+ {col.field === groupingField ? '' : `${groupingField}: `}
101
+ {String(groupingValue)}
102
+ {descendantCount !== undefined ? ` (${descendantCount})` : ''}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <div style={{ display: 'flex', alignItems: 'center', paddingLeft: depth * 24, width: '100%', height: '100%' }}>
109
+ <div style={{ marginRight: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
110
+ {hasChildren ? (
111
+ <div
112
+ onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row.id); }}
113
+ onMouseDown={(e) => { e.stopPropagation(); }}
114
+ style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'auto', zIndex: 10, position: 'relative' }}
115
+ >
116
+ <ExpandIcon
117
+ isExpanded={isExpanded}
118
+ onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(cellParams.row.id); }}
119
+ />
120
+ </div>
121
+ ) : null}
122
+ </div>
123
+ <div style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{content}</div>
124
+ </div>
125
+ );
126
+ }
127
+ };
128
+ }
129
+
130
+ return {
131
+ ...col,
132
+ renderCell: (cellParams: GridRenderCellParams<R>) => {
133
+ const r = cellParams.row as Record<string, unknown>;
134
+ const hasChildren = Boolean(r._hasChildren);
135
+ const groupingField = typeof r._groupingField === 'string' ? r._groupingField : undefined;
136
+
137
+ if (isRowGrouping && hasChildren) {
138
+ if (col.field === groupingField) return null;
139
+ if (cellParams.value !== undefined && cellParams.value !== null) {
140
+ return col.renderCell ? col.renderCell(cellParams) : cellParams.value;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ return col.renderCell ? col.renderCell(cellParams) : cellParams.value;
146
+ }
147
+ };
148
+ }) as GridColDef<R>[];
149
+ }, [activeColumns, isHierarchyEnabled, isRowGrouping, isTreeData, activeHierarchyHandlers]);
150
+
151
+ // ── Column widths ─────────────────────────────────────────────────────────
152
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
153
+ () => initialState?.columns?.columnWidths ?? {}
154
+ );
155
+
156
+ const handleColumnResize = useCallback((field: string, newWidth: number) => {
157
+ setColumnWidths(prev => ({ ...prev, [field]: newWidth }));
158
+ }, []);
159
+
160
+ // ── Column order ──────────────────────────────────────────────────────────
161
+ const [internalColumnOrder, setInternalColumnOrder] = useState<string[]>(
162
+ () => initialState?.columns?.columnOrder ?? activeColumns.map(col => col.field)
163
+ );
164
+
165
+ const effectiveColumnOrder = columnOrder ?? internalColumnOrder;
166
+
167
+ useEffect(() => {
168
+ setColumns(activeColumns as unknown as GridColDef[]);
169
+ }, [activeColumns, setColumns]);
170
+
171
+ useEffect(() => {
172
+ if (pivotMode) {
173
+ setInternalColumnOrder(activeColumns.map(col => col.field));
174
+ }
175
+ }, [pivotMode, activeColumns]);
176
+
177
+ // ── Ordered / visible columns ─────────────────────────────────────────────
178
+ const orderedColumns = useMemo<GridColDef<R>[]>(() => {
179
+ if (disableColumnReorder) return effectiveColumns;
180
+
181
+ const orderMap = new Map(effectiveColumnOrder.map((field, idx) => [field, idx]));
182
+ return [...effectiveColumns].sort((a, b) => {
183
+ const ai = orderMap.get(a.field) ?? effectiveColumns.indexOf(a);
184
+ const bi = orderMap.get(b.field) ?? effectiveColumns.indexOf(b);
185
+ return ai - bi;
186
+ });
187
+ }, [effectiveColumns, effectiveColumnOrder, disableColumnReorder]);
188
+
189
+ const visibleOrderedColumns = useMemo<GridColDef<R>[]>(
190
+ () => orderedColumns.filter(col => columnVisibilityModel[col.field] !== false),
191
+ [orderedColumns, columnVisibilityModel]
192
+ );
193
+
194
+ // ── Column reorder handlers ───────────────────────────────────────────────
195
+ const columnReorderHandlers = useColumnReorder({
196
+ columns: orderedColumns,
197
+ onColumnOrderChange: useCallback((reorderParams: GridColumnOrderChangeParams) => {
198
+ const { oldIndex, targetIndex } = reorderParams;
199
+ const newOrder = [...effectiveColumnOrder];
200
+ const [movedField] = newOrder.splice(oldIndex, 1);
201
+ newOrder.splice(targetIndex, 0, movedField);
202
+ if (!columnOrder) setInternalColumnOrder(newOrder);
203
+ onColumnOrderChange?.(reorderParams);
204
+ }, [effectiveColumnOrder, columnOrder, onColumnOrderChange]),
205
+ disableColumnReorder,
206
+ });
207
+
208
+ // ── Navigation columns (system cols + data cols for keyboard nav) ─────────
209
+ const navigationColumns = useMemo(() => {
210
+ const specials: { field: string }[] = [];
211
+ if (rowReordering) specials.push({ field: '__reorder_col__' });
212
+ if (hasDetailPanel) specials.push({ field: '__expand_col__' });
213
+ if (checkboxSelection) specials.push({ field: '__checkbox_col__' });
214
+ return [...specials, ...orderedColumns] as Array<GridColDef<R> | { field: string }>;
215
+ }, [orderedColumns, checkboxSelection, hasDetailPanel, rowReordering]);
216
+
217
+ return {
218
+ effectiveColumns,
219
+ orderedColumns,
220
+ visibleOrderedColumns,
221
+ navigationColumns,
222
+ columnWidths,
223
+ effectiveColumnOrder,
224
+ setInternalColumnOrder,
225
+ columnReorderHandlers,
226
+ handleColumnResize,
227
+ };
228
+ }
@@ -0,0 +1,192 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import type {
3
+ GridRowId,
4
+ GridSortItem,
5
+ GridColumnPinning,
6
+ GridPaginationModel,
7
+ GridAggregationModel,
8
+ GridPivotModel,
9
+ } from '../../types';
10
+ import type { GridInitialState } from '../../state/types';
11
+
12
+ export interface UseGridControlledStateParams {
13
+ initialState?: GridInitialState;
14
+
15
+ sortModel?: GridSortItem[];
16
+ onSortModelChange?: (model: GridSortItem[]) => void;
17
+
18
+ aggregationModel?: GridAggregationModel;
19
+ onAggregationModelChange?: (model: GridAggregationModel) => void;
20
+
21
+ columnVisibilityModel?: Record<string, boolean>;
22
+ onColumnVisibilityModelChange?: (model: Record<string, boolean>) => void;
23
+
24
+ pinnedColumns?: GridColumnPinning;
25
+ onPinnedColumnsChange?: (model: GridColumnPinning) => void;
26
+
27
+ pivotModel?: GridPivotModel;
28
+ onPivotModelChange?: (model: GridPivotModel) => void;
29
+
30
+ paginationModel?: GridPaginationModel;
31
+ onPaginationModelChange?: (model: GridPaginationModel) => void;
32
+
33
+ rowSelectionModel?: GridRowId[];
34
+ onRowSelectionModelChange?: (model: GridRowId[]) => void;
35
+ }
36
+
37
+ export interface UseGridControlledStateReturn {
38
+ // sort
39
+ sortModel: GridSortItem[];
40
+ isSortControlled: boolean;
41
+ setInternalSortModel: React.Dispatch<React.SetStateAction<GridSortItem[]>>;
42
+
43
+ // aggregation
44
+ aggregationModel: GridAggregationModel;
45
+ handleAggregationModelChange: (model: GridAggregationModel) => void;
46
+
47
+ // column visibility
48
+ columnVisibilityModel: Record<string, boolean>;
49
+ handleColumnVisibilityModelChange: (model: Record<string, boolean>) => void;
50
+
51
+ // pinned columns
52
+ pinnedColumns: GridColumnPinning;
53
+ handlePinnedColumnsChange: (model: GridColumnPinning) => void;
54
+
55
+ // pivot
56
+ currentPivotModel: GridPivotModel;
57
+ handlePivotModelChange: (model: GridPivotModel) => void;
58
+
59
+ // pagination
60
+ effectivePaginationModel: GridPaginationModel;
61
+ handlePaginationModelChange: (model: GridPaginationModel) => void;
62
+
63
+ // selection
64
+ rowSelectionModel: GridRowId[];
65
+ selectedRowIds: Set<GridRowId>;
66
+ isSelectionControlled: boolean;
67
+ setInternalRowSelectionModel: React.Dispatch<React.SetStateAction<GridRowId[]>>;
68
+ }
69
+
70
+ const EMPTY_PIVOT_MODEL: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
71
+
72
+ export function useGridControlledState(params: UseGridControlledStateParams): UseGridControlledStateReturn {
73
+ const {
74
+ initialState,
75
+ sortModel: propSortModel,
76
+ aggregationModel: propAggregationModel,
77
+ onAggregationModelChange,
78
+ columnVisibilityModel: propColumnVisibilityModel,
79
+ onColumnVisibilityModelChange,
80
+ pinnedColumns: propPinnedColumns,
81
+ onPinnedColumnsChange,
82
+ pivotModel: propPivotModel,
83
+ onPivotModelChange,
84
+ paginationModel: propPaginationModel = { page: 0, pageSize: 100 },
85
+ onPaginationModelChange,
86
+ rowSelectionModel: propRowSelectionModel,
87
+ onRowSelectionModelChange,
88
+ } = params;
89
+
90
+ // ── Sort ─────────────────────────────────────────────────────────────────
91
+ const [internalSortModel, setInternalSortModel] = useState<GridSortItem[]>(
92
+ () => initialState?.sorting?.sortModel ?? []
93
+ );
94
+ const isSortControlled = propSortModel !== undefined;
95
+ const sortModel = isSortControlled ? propSortModel! : internalSortModel;
96
+
97
+ // ── Aggregation ──────────────────────────────────────────────────────────
98
+ const isAggregationControlled = propAggregationModel !== undefined;
99
+ const [internalAggregationModel, setInternalAggregationModel] = useState<GridAggregationModel>(
100
+ () => propAggregationModel ?? {}
101
+ );
102
+ const aggregationModel = isAggregationControlled ? propAggregationModel! : internalAggregationModel;
103
+
104
+ const handleAggregationModelChange = useCallback((model: GridAggregationModel) => {
105
+ if (!isAggregationControlled) setInternalAggregationModel(model);
106
+ onAggregationModelChange?.(model);
107
+ }, [isAggregationControlled, onAggregationModelChange]);
108
+
109
+ // ── Column visibility ────────────────────────────────────────────────────
110
+ const isColumnVisibilityControlled = propColumnVisibilityModel !== undefined;
111
+ const [internalColumnVisibilityModel, setInternalColumnVisibilityModel] = useState<Record<string, boolean>>(
112
+ () => initialState?.columns?.columnVisibilityModel ?? {}
113
+ );
114
+ const columnVisibilityModel = isColumnVisibilityControlled
115
+ ? propColumnVisibilityModel!
116
+ : internalColumnVisibilityModel;
117
+
118
+ const handleColumnVisibilityModelChange = useCallback((model: Record<string, boolean>) => {
119
+ if (!isColumnVisibilityControlled) setInternalColumnVisibilityModel(model);
120
+ onColumnVisibilityModelChange?.(model);
121
+ }, [isColumnVisibilityControlled, onColumnVisibilityModelChange]);
122
+
123
+ // ── Pinned columns ───────────────────────────────────────────────────────
124
+ const isPinnedColumnsControlled = propPinnedColumns !== undefined;
125
+ const [internalPinnedColumns, setInternalPinnedColumns] = useState<GridColumnPinning>(
126
+ () => initialState?.columns?.pinnedColumns ?? {}
127
+ );
128
+ const pinnedColumns = isPinnedColumnsControlled ? propPinnedColumns! : internalPinnedColumns;
129
+
130
+ const handlePinnedColumnsChange = useCallback((model: GridColumnPinning) => {
131
+ if (!isPinnedColumnsControlled) setInternalPinnedColumns(model);
132
+ onPinnedColumnsChange?.(model);
133
+ }, [isPinnedColumnsControlled, onPinnedColumnsChange]);
134
+
135
+ // ── Pivot ────────────────────────────────────────────────────────────────
136
+ const [internalPivotModel, setInternalPivotModel] = useState<GridPivotModel>(
137
+ () => propPivotModel ?? EMPTY_PIVOT_MODEL
138
+ );
139
+ const currentPivotModel = propPivotModel ?? internalPivotModel;
140
+
141
+ const handlePivotModelChange = useCallback((model: GridPivotModel) => {
142
+ setInternalPivotModel(model);
143
+ onPivotModelChange?.(model);
144
+ }, [onPivotModelChange]);
145
+
146
+ // ── Pagination ───────────────────────────────────────────────────────────
147
+ const isPaginationControlled = propPaginationModel !== undefined && params.paginationModel !== undefined;
148
+ const [internalPaginationModel, setInternalPaginationModel] = useState<GridPaginationModel>(
149
+ () => initialState?.pagination?.paginationModel ?? propPaginationModel
150
+ );
151
+ const effectivePaginationModel = isPaginationControlled ? propPaginationModel : internalPaginationModel;
152
+
153
+ const handlePaginationModelChange = useCallback((newModel: GridPaginationModel) => {
154
+ if (!isPaginationControlled) setInternalPaginationModel(newModel);
155
+ onPaginationModelChange?.(newModel);
156
+ }, [isPaginationControlled, onPaginationModelChange]);
157
+
158
+ // ── Row selection ────────────────────────────────────────────────────────
159
+ const isSelectionControlled = propRowSelectionModel !== undefined;
160
+ const [internalRowSelectionModel, setInternalRowSelectionModel] = useState<GridRowId[]>([]);
161
+ const rowSelectionModel = isSelectionControlled ? propRowSelectionModel! : internalRowSelectionModel;
162
+ const selectedRowIds = useMemo(() => new Set(rowSelectionModel), [rowSelectionModel]);
163
+
164
+ // onRowSelectionModelChange is consumed by the caller directly (they call it when selection changes)
165
+ void onRowSelectionModelChange;
166
+
167
+ return {
168
+ sortModel: sortModel as GridSortItem[],
169
+ isSortControlled,
170
+ setInternalSortModel: setInternalSortModel as React.Dispatch<React.SetStateAction<GridSortItem[]>>,
171
+
172
+ aggregationModel,
173
+ handleAggregationModelChange,
174
+
175
+ columnVisibilityModel,
176
+ handleColumnVisibilityModelChange,
177
+
178
+ pinnedColumns,
179
+ handlePinnedColumnsChange,
180
+
181
+ currentPivotModel,
182
+ handlePivotModelChange,
183
+
184
+ effectivePaginationModel,
185
+ handlePaginationModelChange,
186
+
187
+ rowSelectionModel,
188
+ selectedRowIds,
189
+ isSelectionControlled,
190
+ setInternalRowSelectionModel,
191
+ };
192
+ }
@@ -0,0 +1,109 @@
1
+ import { useMemo } from 'react';
2
+ import { filterRows } from '../../utils/filtering';
3
+ import { sortRows } from '../../utils/sorting';
4
+ import { getPinnedRowGroups } from '../../utils/pinning';
5
+ import type { GridRowModel, GridFilterModel, GridSortItem, GridPaginationModel, GridRowPinning, GridDataSource } from '../../types';
6
+
7
+ interface HierarchyHandlers<R extends GridRowModel> {
8
+ getVisibleRows: () => R[];
9
+ }
10
+
11
+ export interface UseGridRowPipelineParams<R extends GridRowModel> {
12
+ effectiveRows: R[];
13
+ activeHierarchyHandlers: HierarchyHandlers<R> | null;
14
+ filterMode: string;
15
+ filterModel: GridFilterModel;
16
+ dataSource?: GridDataSource<R>;
17
+ sortModel: GridSortItem[];
18
+ sortingMode: string;
19
+ pagination: boolean;
20
+ paginationMode: string;
21
+ effectivePaginationModel: GridPaginationModel;
22
+ pinnedRows?: GridRowPinning;
23
+ isLoading: boolean;
24
+ pageSize: number;
25
+ }
26
+
27
+ export interface GridRowPipelineResult<R extends GridRowModel> {
28
+ filteredRows: R[];
29
+ pinnedTopRows: R[];
30
+ unpinnedRows: R[];
31
+ pinnedBottomRows: R[];
32
+ sortedUnpinnedRows: R[];
33
+ paginatedUnpinnedRows: R[];
34
+ allRenderableRows: R[];
35
+ }
36
+
37
+ export function useGridRowPipeline<R extends GridRowModel>(
38
+ params: UseGridRowPipelineParams<R>
39
+ ): GridRowPipelineResult<R> {
40
+ const {
41
+ effectiveRows,
42
+ activeHierarchyHandlers,
43
+ filterMode,
44
+ filterModel,
45
+ dataSource,
46
+ sortModel,
47
+ sortingMode,
48
+ pagination,
49
+ paginationMode,
50
+ effectivePaginationModel,
51
+ pinnedRows,
52
+ isLoading,
53
+ pageSize,
54
+ } = params;
55
+
56
+ const filteredRows = useMemo<R[]>(() => {
57
+ if (activeHierarchyHandlers) return (activeHierarchyHandlers.getVisibleRows() || []) as R[];
58
+ if (filterMode === 'server' && dataSource) return effectiveRows;
59
+ return filterRows(effectiveRows, filterModel) as R[];
60
+ }, [effectiveRows, filterModel, activeHierarchyHandlers, filterMode, dataSource]);
61
+
62
+ const { top: pinnedTopRows, center: unpinnedRows, bottom: pinnedBottomRows } = useMemo(() => {
63
+ if (activeHierarchyHandlers) return { top: [] as R[], center: filteredRows, bottom: [] as R[] };
64
+ return getPinnedRowGroups(filteredRows, pinnedRows);
65
+ }, [filteredRows, pinnedRows, activeHierarchyHandlers]);
66
+
67
+ const sortedUnpinnedRows = useMemo<R[]>(() => {
68
+ if (activeHierarchyHandlers) return unpinnedRows;
69
+ if (sortingMode === 'server' && dataSource) return unpinnedRows;
70
+ return sortRows(unpinnedRows, sortModel) as R[];
71
+ }, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, dataSource]);
72
+
73
+ const paginatedUnpinnedRows = useMemo<R[]>(() => {
74
+ if (!pagination) return sortedUnpinnedRows;
75
+ if (paginationMode === 'server' && dataSource) return sortedUnpinnedRows;
76
+ const start = effectivePaginationModel.page * effectivePaginationModel.pageSize;
77
+ return sortedUnpinnedRows.slice(start, start + effectivePaginationModel.pageSize);
78
+ }, [sortedUnpinnedRows, pagination, effectivePaginationModel.page, effectivePaginationModel.pageSize, paginationMode, dataSource]);
79
+
80
+ const allRenderableRows = useMemo<R[]>(() => {
81
+ if (activeHierarchyHandlers) return unpinnedRows;
82
+ const centerRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
83
+ const base = [...pinnedTopRows, ...centerRows, ...pinnedBottomRows];
84
+
85
+ if (paginationMode === 'infinite' && isLoading && base.length > 0) {
86
+ const skeletonCount = Math.min(pageSize, 20);
87
+ const skeletons = Array.from({ length: skeletonCount }, (_, i) => ({
88
+ id: `__skeleton_${i}__`,
89
+ _isSkeleton: true,
90
+ }));
91
+ return [...base, ...skeletons] as R[];
92
+ }
93
+
94
+ return base;
95
+ }, [
96
+ pinnedTopRows, paginatedUnpinnedRows, sortedUnpinnedRows, pinnedBottomRows,
97
+ pagination, activeHierarchyHandlers, unpinnedRows, paginationMode, isLoading, pageSize,
98
+ ]);
99
+
100
+ return {
101
+ filteredRows,
102
+ pinnedTopRows,
103
+ unpinnedRows,
104
+ pinnedBottomRows,
105
+ sortedUnpinnedRows,
106
+ paginatedUnpinnedRows,
107
+ allRenderableRows,
108
+ };
109
+ }
@@ -0,0 +1,52 @@
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
+ import type { GridRowScrollEndParams } from '../../types';
3
+
4
+ export interface UseGridScrollSyncParams {
5
+ onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
6
+ }
7
+
8
+ export interface UseGridScrollSyncResult {
9
+ scrollTop: number;
10
+ scrollLeft: number;
11
+ handleScroll: (event: React.UIEvent<HTMLDivElement>) => void;
12
+ }
13
+
14
+ export function useGridScrollSync(params: UseGridScrollSyncParams): UseGridScrollSyncResult {
15
+ const { onRowsScrollEnd } = params;
16
+
17
+ const pendingScrollRef = useRef({ scrollTop: 0, scrollLeft: 0 });
18
+ const scrollRafRef = useRef<number | null>(null);
19
+ const [scrollPos, setScrollPos] = useState({ scrollTop: 0, scrollLeft: 0 });
20
+
21
+ useEffect(() => {
22
+ return () => {
23
+ if (scrollRafRef.current !== null) cancelAnimationFrame(scrollRafRef.current);
24
+ };
25
+ }, []);
26
+
27
+ const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
28
+ const target = event.currentTarget;
29
+ pendingScrollRef.current = { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft };
30
+
31
+ if (scrollRafRef.current !== null) cancelAnimationFrame(scrollRafRef.current);
32
+ scrollRafRef.current = requestAnimationFrame(() => {
33
+ scrollRafRef.current = null;
34
+ setScrollPos(pendingScrollRef.current);
35
+ });
36
+
37
+ if (onRowsScrollEnd) {
38
+ const { scrollTop, scrollHeight, clientHeight } = target;
39
+ const scrollThreshold = 100;
40
+
41
+ if (scrollHeight - scrollTop - clientHeight < scrollThreshold) {
42
+ onRowsScrollEnd({
43
+ visibleTop: scrollTop,
44
+ visibleBottom: scrollTop + clientHeight,
45
+ viewportHeight: clientHeight,
46
+ });
47
+ }
48
+ }
49
+ }, [onRowsScrollEnd]);
50
+
51
+ return { scrollTop: scrollPos.scrollTop, scrollLeft: scrollPos.scrollLeft, handleScroll };
52
+ }
@@ -0,0 +1,63 @@
1
+ import { useRef, useEffect, useLayoutEffect } from 'react';
2
+ import type { GridState } from '../../state/types';
3
+ import type {
4
+ GridSortItem,
5
+ GridFilterModel,
6
+ GridPaginationModel,
7
+ GridColumnPinning,
8
+ } from '../../types';
9
+
10
+ export interface UseGridStateSnapshotParams {
11
+ onStateChange?: (state: GridState) => void;
12
+ sortModel: GridSortItem[];
13
+ filterModel: GridFilterModel;
14
+ effectivePaginationModel: GridPaginationModel;
15
+ columnWidths: Record<string, number>;
16
+ effectiveColumnOrder: string[];
17
+ columnVisibilityModel: Record<string, boolean>;
18
+ pinnedColumns: GridColumnPinning;
19
+ }
20
+
21
+ export function useGridStateSnapshot(params: UseGridStateSnapshotParams): void {
22
+ const {
23
+ onStateChange,
24
+ sortModel,
25
+ filterModel,
26
+ effectivePaginationModel,
27
+ columnWidths,
28
+ effectiveColumnOrder,
29
+ columnVisibilityModel,
30
+ pinnedColumns,
31
+ } = params;
32
+
33
+ const onStateChangeRef = useRef(onStateChange);
34
+ useLayoutEffect(() => {
35
+ onStateChangeRef.current = onStateChange;
36
+ });
37
+
38
+ useEffect(() => {
39
+ if (!onStateChangeRef.current) return;
40
+
41
+ const snapshot: GridState = {
42
+ sorting: { sortModel: sortModel as { field: string; sort: 'asc' | 'desc' }[] },
43
+ filter: { filterModel },
44
+ pagination: { paginationModel: effectivePaginationModel },
45
+ columns: {
46
+ columnWidths,
47
+ columnOrder: effectiveColumnOrder,
48
+ columnVisibilityModel,
49
+ pinnedColumns,
50
+ },
51
+ };
52
+
53
+ onStateChangeRef.current(snapshot);
54
+ }, [
55
+ sortModel,
56
+ filterModel,
57
+ effectivePaginationModel,
58
+ columnWidths,
59
+ effectiveColumnOrder,
60
+ columnVisibilityModel,
61
+ pinnedColumns,
62
+ ]);
63
+ }