@opencorestack/opengridx 0.1.6 → 0.1.8
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 +30 -1
- package/README.md +9 -0
- package/dist/index.d.ts +12 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +2505 -2450
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/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 +2269 -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 +214 -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 +110 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
- package/lib/components/Toolbar/GridToolbar.css +221 -0
- package/lib/components/Toolbar/GridToolbar.tsx +658 -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 +174 -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 +2 -1
- package/package.json +12 -4
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
|
|
2
|
+
import { useReducer, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
GridState,
|
|
5
|
+
GridRowModel,
|
|
6
|
+
GridColDef,
|
|
7
|
+
GridRowId,
|
|
8
|
+
GridSortItem,
|
|
9
|
+
GridFilterModel,
|
|
10
|
+
GridPaginationModel,
|
|
11
|
+
GridColumnVisibilityModel,
|
|
12
|
+
GridApi
|
|
13
|
+
} from '../../types';
|
|
14
|
+
|
|
15
|
+
type GridAction =
|
|
16
|
+
| { type: 'SET_ROWS'; payload: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]); preserveRowCount?: boolean }
|
|
17
|
+
| { type: 'SET_COLUMNS'; payload: GridColDef[] }
|
|
18
|
+
| { type: 'SET_SORT_MODEL'; payload: GridSortItem[] }
|
|
19
|
+
| { type: 'SET_FILTER_MODEL'; payload: GridFilterModel }
|
|
20
|
+
| { type: 'SET_PAGINATION_MODEL'; payload: GridPaginationModel }
|
|
21
|
+
| { type: 'SET_SELECTION'; payload: Set<GridRowId> }
|
|
22
|
+
| { type: 'SET_SCROLL'; payload: { scrollTop: number; scrollLeft: number } }
|
|
23
|
+
| { type: 'SET_DIMENSIONS'; payload: { viewportWidth: number; viewportHeight: number } }
|
|
24
|
+
| { type: 'SET_DATASOURCE_LOADING'; payload: boolean }
|
|
25
|
+
| { type: 'SET_DATASOURCE_ERROR'; payload: any }
|
|
26
|
+
| { type: 'SET_ROW_COUNT'; payload: number };
|
|
27
|
+
|
|
28
|
+
function createInitialState(rows: GridRowModel[], columns: GridColDef[], columnVisibilityModel: GridColumnVisibilityModel = {}): GridState {
|
|
29
|
+
const idRowsLookup = new Map<GridRowId, GridRowModel>();
|
|
30
|
+
const allRows: GridRowId[] = [];
|
|
31
|
+
|
|
32
|
+
rows.forEach(row => {
|
|
33
|
+
const id = row.id;
|
|
34
|
+
idRowsLookup.set(id, row);
|
|
35
|
+
allRows.push(id);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const columnLookup = new Map<string, GridColDef>();
|
|
39
|
+
const orderedFields: string[] = [];
|
|
40
|
+
|
|
41
|
+
columns.forEach(col => {
|
|
42
|
+
columnLookup.set(col.field, col);
|
|
43
|
+
orderedFields.push(col.field);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
rows: {
|
|
48
|
+
idRowsLookup,
|
|
49
|
+
allRows
|
|
50
|
+
},
|
|
51
|
+
columns: {
|
|
52
|
+
all: columns,
|
|
53
|
+
lookup: columnLookup,
|
|
54
|
+
orderedFields,
|
|
55
|
+
columnVisibilityModel
|
|
56
|
+
},
|
|
57
|
+
sorting: {
|
|
58
|
+
sortModel: []
|
|
59
|
+
},
|
|
60
|
+
filter: {
|
|
61
|
+
filterModel: { items: [] }
|
|
62
|
+
},
|
|
63
|
+
pagination: {
|
|
64
|
+
paginationModel: { page: 0, pageSize: 100 },
|
|
65
|
+
rowCount: rows.length
|
|
66
|
+
},
|
|
67
|
+
selection: {
|
|
68
|
+
selectedRows: new Set()
|
|
69
|
+
},
|
|
70
|
+
pinning: {
|
|
71
|
+
pinnedColumns: {},
|
|
72
|
+
pinnedRows: {}
|
|
73
|
+
},
|
|
74
|
+
virtualization: {
|
|
75
|
+
renderContext: {
|
|
76
|
+
firstRowIndex: 0,
|
|
77
|
+
lastRowIndex: 0,
|
|
78
|
+
firstColumnIndex: 0,
|
|
79
|
+
lastColumnIndex: 0
|
|
80
|
+
},
|
|
81
|
+
scrollTop: 0,
|
|
82
|
+
scrollLeft: 0
|
|
83
|
+
},
|
|
84
|
+
dimensions: {
|
|
85
|
+
rowHeight: 52,
|
|
86
|
+
headerHeight: 56,
|
|
87
|
+
viewportWidth: 0,
|
|
88
|
+
viewportHeight: 0
|
|
89
|
+
},
|
|
90
|
+
dataSource: {
|
|
91
|
+
loading: false,
|
|
92
|
+
error: null
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function gridReducer(state: GridState, action: GridAction): GridState {
|
|
98
|
+
switch (action.type) {
|
|
99
|
+
case 'SET_ROWS': {
|
|
100
|
+
const currentRows = Array.from(state.rows.idRowsLookup.values());
|
|
101
|
+
const newRows = typeof action.payload === 'function'
|
|
102
|
+
? action.payload(currentRows)
|
|
103
|
+
: action.payload;
|
|
104
|
+
|
|
105
|
+
const idRowsLookup = new Map<GridRowId, GridRowModel>();
|
|
106
|
+
const allRows: GridRowId[] = [];
|
|
107
|
+
|
|
108
|
+
newRows.forEach(row => {
|
|
109
|
+
idRowsLookup.set(row.id, row);
|
|
110
|
+
allRows.push(row.id);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...state,
|
|
115
|
+
rows: { idRowsLookup, allRows },
|
|
116
|
+
pagination: {
|
|
117
|
+
...state.pagination,
|
|
118
|
+
// Only override rowCount when we're NOT in server mode (i.e. no preserveRowCount flag)
|
|
119
|
+
// Server-side fetches set rowCount separately via SET_ROW_COUNT.
|
|
120
|
+
rowCount: action.preserveRowCount ? state.pagination.rowCount : newRows.length
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'SET_COLUMNS': {
|
|
126
|
+
const columnLookup = new Map<string, GridColDef>();
|
|
127
|
+
const orderedFields: string[] = [];
|
|
128
|
+
|
|
129
|
+
action.payload.forEach(col => {
|
|
130
|
+
columnLookup.set(col.field, col);
|
|
131
|
+
orderedFields.push(col.field);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...state,
|
|
136
|
+
columns: {
|
|
137
|
+
all: action.payload,
|
|
138
|
+
lookup: columnLookup,
|
|
139
|
+
orderedFields,
|
|
140
|
+
columnVisibilityModel: {}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'SET_SORT_MODEL':
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
sorting: { sortModel: action.payload }
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
case 'SET_FILTER_MODEL':
|
|
152
|
+
return {
|
|
153
|
+
...state,
|
|
154
|
+
filter: { filterModel: action.payload }
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
case 'SET_PAGINATION_MODEL':
|
|
158
|
+
return {
|
|
159
|
+
...state,
|
|
160
|
+
pagination: {
|
|
161
|
+
...state.pagination,
|
|
162
|
+
paginationModel: action.payload
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
case 'SET_SELECTION':
|
|
167
|
+
return {
|
|
168
|
+
...state,
|
|
169
|
+
selection: { selectedRows: action.payload }
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
case 'SET_SCROLL':
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
virtualization: {
|
|
176
|
+
...state.virtualization,
|
|
177
|
+
scrollTop: action.payload.scrollTop,
|
|
178
|
+
scrollLeft: action.payload.scrollLeft
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
case 'SET_DIMENSIONS':
|
|
183
|
+
return {
|
|
184
|
+
...state,
|
|
185
|
+
dimensions: {
|
|
186
|
+
...state.dimensions,
|
|
187
|
+
viewportWidth: action.payload.viewportWidth,
|
|
188
|
+
viewportHeight: action.payload.viewportHeight
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
case 'SET_DATASOURCE_LOADING':
|
|
193
|
+
return {
|
|
194
|
+
...state,
|
|
195
|
+
dataSource: {
|
|
196
|
+
...state.dataSource,
|
|
197
|
+
loading: action.payload
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
case 'SET_DATASOURCE_ERROR':
|
|
202
|
+
return {
|
|
203
|
+
...state,
|
|
204
|
+
dataSource: {
|
|
205
|
+
...state.dataSource,
|
|
206
|
+
error: action.payload
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
case 'SET_ROW_COUNT':
|
|
211
|
+
return {
|
|
212
|
+
...state,
|
|
213
|
+
pagination: {
|
|
214
|
+
...state.pagination,
|
|
215
|
+
rowCount: action.payload
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface UseDataGridParams<R extends GridRowModel = GridRowModel> {
|
|
225
|
+
rows: R[];
|
|
226
|
+
columns: GridColDef<R>[];
|
|
227
|
+
rowHeight?: number;
|
|
228
|
+
headerHeight?: number;
|
|
229
|
+
rowCount?: number;
|
|
230
|
+
columnVisibilityModel?: GridColumnVisibilityModel;
|
|
231
|
+
initialState?: import('../../state/types').GridInitialState;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function useDataGrid<R extends GridRowModel = GridRowModel>(params: UseDataGridParams<R>) {
|
|
235
|
+
const { rows, columns, rowHeight = 52, headerHeight = 56, columnVisibilityModel, initialState: propInitialState } = params;
|
|
236
|
+
|
|
237
|
+
const internalInitialState = useMemo(
|
|
238
|
+
() => createInitialState(rows, columns as any, columnVisibilityModel),
|
|
239
|
+
[]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const [state, dispatch] = useReducer(gridReducer, {
|
|
243
|
+
...internalInitialState,
|
|
244
|
+
sorting: propInitialState?.sorting || internalInitialState.sorting,
|
|
245
|
+
filter: propInitialState?.filter || internalInitialState.filter,
|
|
246
|
+
dataSource: (propInitialState?.dataSource
|
|
247
|
+
? { ...internalInitialState.dataSource, ...propInitialState.dataSource }
|
|
248
|
+
: internalInitialState.dataSource) as any,
|
|
249
|
+
pagination: {
|
|
250
|
+
...internalInitialState.pagination,
|
|
251
|
+
...(propInitialState?.pagination || {}),
|
|
252
|
+
rowCount: params.rowCount ?? propInitialState?.pagination?.rowCount ?? internalInitialState.pagination.rowCount
|
|
253
|
+
},
|
|
254
|
+
dimensions: {
|
|
255
|
+
...internalInitialState.dimensions,
|
|
256
|
+
rowHeight,
|
|
257
|
+
headerHeight
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const stateRef = useRef(state);
|
|
262
|
+
stateRef.current = state;
|
|
263
|
+
|
|
264
|
+
// Sync internal state when the external `rows` prop changes
|
|
265
|
+
const prevRowsRef = useRef(rows);
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (rows !== prevRowsRef.current) {
|
|
268
|
+
prevRowsRef.current = rows;
|
|
269
|
+
dispatch({ type: 'SET_ROWS', payload: rows as GridRowModel[] });
|
|
270
|
+
}
|
|
271
|
+
}, [rows]);
|
|
272
|
+
|
|
273
|
+
const setRows = useCallback((rowsOrUpdater: GridRowModel[] | ((prev: GridRowModel[]) => GridRowModel[]), preserveRowCount?: boolean) => {
|
|
274
|
+
dispatch({ type: 'SET_ROWS', payload: rowsOrUpdater, preserveRowCount });
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
const setColumns = useCallback((newColumns: GridColDef[]) => {
|
|
278
|
+
dispatch({ type: 'SET_COLUMNS', payload: newColumns });
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
const setSortModel = useCallback((sortModel: GridSortItem[]) => {
|
|
282
|
+
dispatch({ type: 'SET_SORT_MODEL', payload: sortModel });
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
const setFilterModel = useCallback((filterModel: GridFilterModel) => {
|
|
286
|
+
dispatch({ type: 'SET_FILTER_MODEL', payload: filterModel });
|
|
287
|
+
}, []);
|
|
288
|
+
|
|
289
|
+
const setPaginationModel = useCallback((paginationModel: GridPaginationModel) => {
|
|
290
|
+
dispatch({ type: 'SET_PAGINATION_MODEL', payload: paginationModel });
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
const setSelection = useCallback((selectedRows: Set<GridRowId>) => {
|
|
294
|
+
dispatch({ type: 'SET_SELECTION', payload: selectedRows });
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
const setScroll = useCallback((scrollTop: number, scrollLeft: number) => {
|
|
298
|
+
dispatch({ type: 'SET_SCROLL', payload: { scrollTop, scrollLeft } });
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
const setDimensions = useCallback((viewportWidth: number, viewportHeight: number) => {
|
|
302
|
+
dispatch({ type: 'SET_DIMENSIONS', payload: { viewportWidth, viewportHeight } });
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
const setDataSourceLoading = useCallback((loading: boolean) => {
|
|
306
|
+
dispatch({ type: 'SET_DATASOURCE_LOADING', payload: loading });
|
|
307
|
+
}, []);
|
|
308
|
+
|
|
309
|
+
const setDataSourceError = useCallback((error: any) => {
|
|
310
|
+
dispatch({ type: 'SET_DATASOURCE_ERROR', payload: error });
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
const setRowCount = useCallback((count: number) => {
|
|
314
|
+
dispatch({ type: 'SET_ROW_COUNT', payload: count });
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
const apiRef = useRef<GridApi>({
|
|
318
|
+
getRow: (id: GridRowId) => stateRef.current.rows.idRowsLookup.get(id) || null,
|
|
319
|
+
getAllRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
|
|
320
|
+
getVisibleRows: () => Array.from(stateRef.current.rows.idRowsLookup.values()),
|
|
321
|
+
getAggregationResult: () => null,
|
|
322
|
+
getAggregationModel: () => null,
|
|
323
|
+
getColumn: (field: string) => stateRef.current.columns.lookup.get(field) || null,
|
|
324
|
+
getAllColumns: () => stateRef.current.columns.all,
|
|
325
|
+
getVisibleColumns: () => stateRef.current.columns.all,
|
|
326
|
+
selectRow: (id: GridRowId, isSelected = true) => {
|
|
327
|
+
const newSelection = new Set(stateRef.current.selection.selectedRows);
|
|
328
|
+
if (isSelected) {
|
|
329
|
+
newSelection.add(id);
|
|
330
|
+
} else {
|
|
331
|
+
newSelection.delete(id);
|
|
332
|
+
}
|
|
333
|
+
setSelection(newSelection);
|
|
334
|
+
},
|
|
335
|
+
selectRows: (ids: GridRowId[], isSelected = true) => {
|
|
336
|
+
const newSelection = new Set(stateRef.current.selection.selectedRows);
|
|
337
|
+
ids.forEach(id => {
|
|
338
|
+
if (isSelected) {
|
|
339
|
+
newSelection.add(id);
|
|
340
|
+
} else {
|
|
341
|
+
newSelection.delete(id);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
setSelection(newSelection);
|
|
345
|
+
},
|
|
346
|
+
getSelectedRows: () => Array.from(stateRef.current.selection.selectedRows),
|
|
347
|
+
sortColumn: (field: string, direction) => {
|
|
348
|
+
if (direction === null) {
|
|
349
|
+
setSortModel(stateRef.current.sorting.sortModel.filter(item => item.field !== field));
|
|
350
|
+
} else {
|
|
351
|
+
const existingIndex = stateRef.current.sorting.sortModel.findIndex(item => item.field === field);
|
|
352
|
+
if (existingIndex >= 0) {
|
|
353
|
+
const newModel = [...stateRef.current.sorting.sortModel];
|
|
354
|
+
newModel[existingIndex] = { field, sort: direction };
|
|
355
|
+
setSortModel(newModel);
|
|
356
|
+
} else {
|
|
357
|
+
setSortModel([...stateRef.current.sorting.sortModel, { field, sort: direction }]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
getSortModel: () => stateRef.current.sorting.sortModel,
|
|
362
|
+
setFilterModel,
|
|
363
|
+
getFilterModel: () => stateRef.current.filter.filterModel,
|
|
364
|
+
setPage: (page: number) => {
|
|
365
|
+
setPaginationModel({ ...stateRef.current.pagination.paginationModel, page });
|
|
366
|
+
},
|
|
367
|
+
setPageSize: (pageSize: number) => {
|
|
368
|
+
setPaginationModel({ ...stateRef.current.pagination.paginationModel, pageSize, page: 0 });
|
|
369
|
+
},
|
|
370
|
+
scrollToIndexes: () => {
|
|
371
|
+
|
|
372
|
+
},
|
|
373
|
+
copySelectedRows: () => Promise.resolve()
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
state,
|
|
378
|
+
dispatch,
|
|
379
|
+
apiRef,
|
|
380
|
+
setRows,
|
|
381
|
+
setColumns,
|
|
382
|
+
setSortModel,
|
|
383
|
+
setFilterModel,
|
|
384
|
+
setPaginationModel,
|
|
385
|
+
setSelection,
|
|
386
|
+
setScroll,
|
|
387
|
+
setDimensions,
|
|
388
|
+
setDataSourceLoading,
|
|
389
|
+
setDataSourceError,
|
|
390
|
+
setRowCount
|
|
391
|
+
};
|
|
392
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import type { GridApi } from '../../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to create a ref for the DataGrid API.
|
|
6
|
+
* This ref can be passed to the `apiRef` prop of the `DataGrid` component.
|
|
7
|
+
*/
|
|
8
|
+
export function useGridApiRef() {
|
|
9
|
+
return useRef<GridApi>(null!);
|
|
10
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
|
|
2
|
+
import { useMemo, useEffect, useRef, useCallback, useState } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
GridRowModel,
|
|
5
|
+
GridAggregationModel,
|
|
6
|
+
GridAggregationResult,
|
|
7
|
+
GridFilterModel,
|
|
8
|
+
GridSortItem,
|
|
9
|
+
GridDataSource,
|
|
10
|
+
} from '../../types';
|
|
11
|
+
|
|
12
|
+
export type BuiltInAggFn = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'unique';
|
|
13
|
+
|
|
14
|
+
const AGGREGATION_FUNCTIONS: Record<BuiltInAggFn, (values: any[]) => any> = {
|
|
15
|
+
sum: (values) => {
|
|
16
|
+
const nums = values.filter((v) => v != null && !isNaN(Number(v)));
|
|
17
|
+
return nums.reduce((acc, v) => acc + Number(v), 0);
|
|
18
|
+
},
|
|
19
|
+
avg: (values) => {
|
|
20
|
+
const nums = values.filter((v) => v != null && !isNaN(Number(v)));
|
|
21
|
+
if (nums.length === 0) return null;
|
|
22
|
+
return nums.reduce((acc, v) => acc + Number(v), 0) / nums.length;
|
|
23
|
+
},
|
|
24
|
+
count: (values) => values.filter((v) => v != null).length,
|
|
25
|
+
min: (values) => {
|
|
26
|
+
const nums = values.filter((v) => v != null && !isNaN(Number(v))).map(Number);
|
|
27
|
+
return nums.length ? Math.min(...nums) : null;
|
|
28
|
+
},
|
|
29
|
+
max: (values) => {
|
|
30
|
+
const nums = values.filter((v) => v != null && !isNaN(Number(v))).map(Number);
|
|
31
|
+
return nums.length ? Math.max(...nums) : null;
|
|
32
|
+
},
|
|
33
|
+
unique: (values) => new Set(values.filter((v) => v != null)).size,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function formatAggregationValue(value: any, fnName: string): string {
|
|
37
|
+
if (value == null) return '—';
|
|
38
|
+
if (typeof value === 'number') {
|
|
39
|
+
if (fnName === 'avg') {
|
|
40
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
41
|
+
}
|
|
42
|
+
return value.toLocaleString();
|
|
43
|
+
}
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UseAggregationParams<R extends GridRowModel> {
|
|
48
|
+
rows: R[];
|
|
49
|
+
aggregationModel: GridAggregationModel;
|
|
50
|
+
isServerSide: boolean;
|
|
51
|
+
dataSource?: GridDataSource<R>;
|
|
52
|
+
filterModel?: GridFilterModel;
|
|
53
|
+
sortModel?: GridSortItem[];
|
|
54
|
+
serverAggregationResults?: GridAggregationResult | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UseAggregationReturn {
|
|
58
|
+
aggregationResult: GridAggregationResult;
|
|
59
|
+
isLoading: boolean;
|
|
60
|
+
error: any;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useAggregation<R extends GridRowModel>(
|
|
64
|
+
params: UseAggregationParams<R>
|
|
65
|
+
): UseAggregationReturn {
|
|
66
|
+
const {
|
|
67
|
+
rows,
|
|
68
|
+
aggregationModel,
|
|
69
|
+
isServerSide,
|
|
70
|
+
dataSource,
|
|
71
|
+
filterModel,
|
|
72
|
+
sortModel,
|
|
73
|
+
serverAggregationResults,
|
|
74
|
+
} = params;
|
|
75
|
+
|
|
76
|
+
const clientResult = useMemo<GridAggregationResult>(() => {
|
|
77
|
+
if (isServerSide || Object.keys(aggregationModel).length === 0) return {};
|
|
78
|
+
|
|
79
|
+
const result: GridAggregationResult = {};
|
|
80
|
+
for (const [field, fnName] of Object.entries(aggregationModel)) {
|
|
81
|
+
const fn = AGGREGATION_FUNCTIONS[fnName as BuiltInAggFn];
|
|
82
|
+
if (!fn) {
|
|
83
|
+
console.warn(`[useAggregation] Unknown aggregation function: "${fnName}"`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const values = rows.map((row) => (row as any)[field]);
|
|
87
|
+
result[field] = fn(values);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}, [rows, aggregationModel, isServerSide]);
|
|
91
|
+
|
|
92
|
+
const [serverResult, setServerResult] = useState<GridAggregationResult>({});
|
|
93
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
94
|
+
const [error, setError] = useState<any>(null);
|
|
95
|
+
const requestIdRef = useRef(0);
|
|
96
|
+
|
|
97
|
+
const fetchServerAggregations = useCallback(async () => {
|
|
98
|
+
if (!dataSource?.getAggregations || Object.keys(aggregationModel).length === 0) return;
|
|
99
|
+
|
|
100
|
+
const requestId = ++requestIdRef.current;
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
setError(null);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await dataSource.getAggregations({
|
|
106
|
+
sortModel: sortModel ?? [],
|
|
107
|
+
filterModel: filterModel ?? { items: [] },
|
|
108
|
+
groupKeys: [],
|
|
109
|
+
aggregationModel,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (requestId === requestIdRef.current) {
|
|
113
|
+
setServerResult(result);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (requestId === requestIdRef.current) {
|
|
117
|
+
setError(err);
|
|
118
|
+
console.error('[useAggregation] Server aggregation error:', err);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
if (requestId === requestIdRef.current) {
|
|
122
|
+
setIsLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, [dataSource, aggregationModel, filterModel, sortModel]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (isServerSide && dataSource?.getAggregations && !serverAggregationResults) {
|
|
129
|
+
fetchServerAggregations();
|
|
130
|
+
}
|
|
131
|
+
}, [fetchServerAggregations, isServerSide, dataSource, serverAggregationResults]);
|
|
132
|
+
|
|
133
|
+
const aggregationResult = useMemo<GridAggregationResult>(() => {
|
|
134
|
+
if (!isServerSide) return clientResult;
|
|
135
|
+
|
|
136
|
+
if (serverAggregationResults && Object.keys(serverAggregationResults).length > 0) {
|
|
137
|
+
return serverAggregationResults;
|
|
138
|
+
}
|
|
139
|
+
return serverResult;
|
|
140
|
+
}, [isServerSide, clientResult, serverAggregationResults, serverResult]);
|
|
141
|
+
|
|
142
|
+
return { aggregationResult, isLoading, error };
|
|
143
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
import type { GridColDef, GridRowModel, GridRowId } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface UseGridClipboardProps {
|
|
5
|
+
/** The currently selected row IDs — must come from the live selection source (not state.selection which is stale). */
|
|
6
|
+
selectedRowIds: Set<GridRowId>;
|
|
7
|
+
columns: GridColDef[];
|
|
8
|
+
getVisibleRows: () => GridRowModel[];
|
|
9
|
+
getRowId: (row: GridRowModel) => GridRowId;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function copyTextSynchronous(text: string): boolean {
|
|
13
|
+
const textarea = document.createElement('textarea');
|
|
14
|
+
textarea.value = text;
|
|
15
|
+
textarea.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
|
|
16
|
+
document.body.appendChild(textarea);
|
|
17
|
+
textarea.focus();
|
|
18
|
+
textarea.select();
|
|
19
|
+
let success = false;
|
|
20
|
+
try {
|
|
21
|
+
success = document.execCommand('copy');
|
|
22
|
+
} finally {
|
|
23
|
+
document.body.removeChild(textarea);
|
|
24
|
+
}
|
|
25
|
+
return success;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Writes text to clipboard.
|
|
30
|
+
* Uses both execCommand (synchronous, needed for non-HTTPS or non-async-gesture environments)
|
|
31
|
+
* and navigator.clipboard for modern browser support.
|
|
32
|
+
*/
|
|
33
|
+
async function writeToClipboard(text: string): Promise<void> {
|
|
34
|
+
let fallbackSuccess = false;
|
|
35
|
+
try {
|
|
36
|
+
fallbackSuccess = copyTextSynchronous(text);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Ignore fallback errors
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
await navigator.clipboard.writeText(text);
|
|
44
|
+
return;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (fallbackSuccess) return;
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
} else if (!fallbackSuccess) {
|
|
50
|
+
throw new Error('Clipboard API not available and fallback failed.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook to handle clipboard operations.
|
|
56
|
+
* Copies selected rows as Tab-Separated Values (TSV) on Ctrl+C / Cmd+C.
|
|
57
|
+
* Also exposes `copySelectedRows` for programmatic use via apiRef.
|
|
58
|
+
*/
|
|
59
|
+
export function useGridClipboard(props: UseGridClipboardProps) {
|
|
60
|
+
const { selectedRowIds, columns, getVisibleRows, getRowId } = props;
|
|
61
|
+
|
|
62
|
+
const copySelectedRows = useCallback(async () => {
|
|
63
|
+
|
|
64
|
+
if (selectedRowIds.size === 0) return;
|
|
65
|
+
|
|
66
|
+
const visibleRows = getVisibleRows();
|
|
67
|
+
const rowsToCopy = visibleRows.filter((row) => selectedRowIds.has(getRowId(row)));
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if (rowsToCopy.length === 0) return;
|
|
72
|
+
|
|
73
|
+
// Exclude internal grid columns (checkbox, expand, reorder)
|
|
74
|
+
const visibleColumns = columns.filter((col) => !col.field.startsWith('__'));
|
|
75
|
+
|
|
76
|
+
// Header row
|
|
77
|
+
const headerRow = visibleColumns
|
|
78
|
+
.map((col) => col.headerName || col.field)
|
|
79
|
+
.join('\t');
|
|
80
|
+
|
|
81
|
+
// Data rows
|
|
82
|
+
const dataRows = rowsToCopy
|
|
83
|
+
.map((row) =>
|
|
84
|
+
visibleColumns
|
|
85
|
+
.map((col) => {
|
|
86
|
+
const value = row[col.field];
|
|
87
|
+
if (col.valueFormatter) {
|
|
88
|
+
return col.valueFormatter({ value, row, field: col.field });
|
|
89
|
+
}
|
|
90
|
+
return value ?? '';
|
|
91
|
+
})
|
|
92
|
+
.join('\t')
|
|
93
|
+
)
|
|
94
|
+
.join('\n');
|
|
95
|
+
|
|
96
|
+
const fullContent = `${headerRow}\n${dataRows}`;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await writeToClipboard(fullContent);
|
|
100
|
+
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[OpenGridX] Failed to copy to clipboard:', err);
|
|
103
|
+
}
|
|
104
|
+
}, [selectedRowIds, columns, getVisibleRows, getRowId]);
|
|
105
|
+
|
|
106
|
+
// Keyboard shortcut: Ctrl+C / Cmd+C
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
109
|
+
if (!(event.ctrlKey || event.metaKey) || event.key !== 'c') return;
|
|
110
|
+
|
|
111
|
+
// Don't intercept when user is typing in an input / editor
|
|
112
|
+
const activeEl = document.activeElement as HTMLElement;
|
|
113
|
+
const tag = activeEl?.tagName.toLowerCase();
|
|
114
|
+
const type = (activeEl as HTMLInputElement)?.type;
|
|
115
|
+
|
|
116
|
+
// Skip capturing Ctrl+C if we are genuinely inside a text input
|
|
117
|
+
if (
|
|
118
|
+
(tag === 'input' && type !== 'checkbox' && type !== 'radio') ||
|
|
119
|
+
tag === 'textarea' ||
|
|
120
|
+
tag === 'select'
|
|
121
|
+
) return;
|
|
122
|
+
|
|
123
|
+
if (activeEl?.isContentEditable) return;
|
|
124
|
+
|
|
125
|
+
copySelectedRows();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
129
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
130
|
+
}, [copySelectedRows]);
|
|
131
|
+
|
|
132
|
+
return { copySelectedRows };
|
|
133
|
+
}
|