@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +12 -1
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +2505 -2450
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +12 -12
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +177 -0
  10. package/docs/README.md +60 -0
  11. package/docs/components/cell.md +44 -0
  12. package/docs/components/column-group-header.md +57 -0
  13. package/docs/components/column-resize.md +63 -0
  14. package/docs/components/column-visibility.md +61 -0
  15. package/docs/components/datagrid.md +70 -0
  16. package/docs/components/filter-panel.md +26 -0
  17. package/docs/components/header.md +39 -0
  18. package/docs/components/pagination.md +34 -0
  19. package/docs/components/row.md +31 -0
  20. package/docs/components/toolbar.md +40 -0
  21. package/docs/components/tooltip.md +68 -0
  22. package/docs/customization/slots-api.md +227 -0
  23. package/docs/customization/theming.md +293 -0
  24. package/docs/features/aggregation-pivot.md +79 -0
  25. package/docs/features/cell-spanning.md +520 -0
  26. package/docs/features/clipboard.md +76 -0
  27. package/docs/features/custom-pagination.md +143 -0
  28. package/docs/features/data-source.md +72 -0
  29. package/docs/features/editing-reordering.md +99 -0
  30. package/docs/features/export-guide.md +434 -0
  31. package/docs/features/filtering.md +83 -0
  32. package/docs/features/infinite-scroll.md +87 -0
  33. package/docs/features/list-view.md +65 -0
  34. package/docs/features/loading-states.md +68 -0
  35. package/docs/features/master-detail.md +46 -0
  36. package/docs/features/pinning.md +67 -0
  37. package/docs/features/selection.md +70 -0
  38. package/docs/features/sorting-pagination.md +66 -0
  39. package/docs/features/state-persistence.md +67 -0
  40. package/docs/features/tree-data-grouping.md +67 -0
  41. package/docs/features/virtualization.md +188 -0
  42. package/docs/roadmap.md +71 -0
  43. package/lib/components/Cell/Cell.css +119 -0
  44. package/lib/components/Cell/Cell.tsx +264 -0
  45. package/lib/components/Cell/GridEditInputCell.css +62 -0
  46. package/lib/components/Cell/GridEditInputCell.tsx +152 -0
  47. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
  48. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
  49. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
  50. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
  51. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
  52. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
  53. package/lib/components/DataGrid/DataGrid.css +224 -0
  54. package/lib/components/DataGrid/DataGrid.tsx +2269 -0
  55. package/lib/components/DetailPanel/DetailPanel.css +36 -0
  56. package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
  57. package/lib/components/FilterPanel/FilterPanel.css +196 -0
  58. package/lib/components/FilterPanel/FilterPanel.tsx +214 -0
  59. package/lib/components/FilterPanel/index.ts +1 -0
  60. package/lib/components/Header/ColumnMenu.css +68 -0
  61. package/lib/components/Header/ColumnMenu.tsx +249 -0
  62. package/lib/components/Header/Header.css +224 -0
  63. package/lib/components/Header/Header.tsx +516 -0
  64. package/lib/components/ListView/ListView.css +112 -0
  65. package/lib/components/ListView/ListViewRow.tsx +63 -0
  66. package/lib/components/Pagination/Pagination.css +91 -0
  67. package/lib/components/Pagination/Pagination.tsx +151 -0
  68. package/lib/components/QuickFilter/QuickFilter.css +23 -0
  69. package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
  70. package/lib/components/Row/Row.css +126 -0
  71. package/lib/components/Row/Row.tsx +377 -0
  72. package/lib/components/Skeleton/Skeleton.css +55 -0
  73. package/lib/components/Skeleton/Skeleton.tsx +22 -0
  74. package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
  75. package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
  76. package/lib/components/SkeletonRow/index.ts +1 -0
  77. package/lib/components/Toolbar/GlobalSearch.css +110 -0
  78. package/lib/components/Toolbar/GlobalSearch.tsx +167 -0
  79. package/lib/components/Toolbar/GridToolbar.css +221 -0
  80. package/lib/components/Toolbar/GridToolbar.tsx +658 -0
  81. package/lib/components/Toolbar/PivotPanel.css +212 -0
  82. package/lib/components/Toolbar/PivotPanel.tsx +239 -0
  83. package/lib/components/Tooltip/Tooltip.css +59 -0
  84. package/lib/components/Tooltip/Tooltip.tsx +106 -0
  85. package/lib/components/ui/Button.css +89 -0
  86. package/lib/components/ui/Button.tsx +39 -0
  87. package/lib/components/ui/Checkbox.css +71 -0
  88. package/lib/components/ui/Checkbox.tsx +66 -0
  89. package/lib/components/ui/DragHandleIcon.css +19 -0
  90. package/lib/components/ui/DragHandleIcon.tsx +18 -0
  91. package/lib/components/ui/ExpandIcon.css +38 -0
  92. package/lib/components/ui/ExpandIcon.tsx +75 -0
  93. package/lib/components/ui/Input.css +59 -0
  94. package/lib/components/ui/Input.tsx +45 -0
  95. package/lib/components/ui/index.ts +15 -0
  96. package/lib/hooks/core/useDataGrid.ts +392 -0
  97. package/lib/hooks/core/useGridApiRef.ts +10 -0
  98. package/lib/hooks/features/useAggregation.ts +143 -0
  99. package/lib/hooks/features/useGridClipboard.ts +133 -0
  100. package/lib/hooks/features/useGridDataSource.ts +190 -0
  101. package/lib/hooks/features/useGridEditing.ts +100 -0
  102. package/lib/hooks/features/useGridSpanning.ts +349 -0
  103. package/lib/hooks/features/usePivot.ts +174 -0
  104. package/lib/hooks/useColumnReorder.ts +114 -0
  105. package/lib/hooks/useRowGrouping.ts +302 -0
  106. package/lib/hooks/useRowReorder.ts +83 -0
  107. package/lib/hooks/useTreeData.ts +323 -0
  108. package/lib/index.ts +106 -0
  109. package/lib/state/index.ts +17 -0
  110. package/lib/state/types.ts +48 -0
  111. package/lib/state/useGridStateStorage.ts +113 -0
  112. package/lib/styles/base/reset.css +21 -0
  113. package/lib/styles/base/variables.css +161 -0
  114. package/lib/styles/opengridx.css +27 -0
  115. package/lib/styles/opengridx.css.d.ts +4 -0
  116. package/lib/theme/DataGridThemeProvider.tsx +276 -0
  117. package/lib/theme/defaultStyles.ts +0 -0
  118. package/lib/theme/index.ts +21 -0
  119. package/lib/theme/presets.ts +148 -0
  120. package/lib/theme/types.ts +155 -0
  121. package/lib/types/index.ts +764 -0
  122. package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
  123. package/lib/utils/export/index.ts +550 -0
  124. package/lib/utils/filtering/index.ts +240 -0
  125. package/lib/utils/pinning/index.ts +282 -0
  126. package/lib/utils/sorting/index.ts +55 -0
  127. package/lib/utils/virtualization/index.ts +106 -0
  128. package/lib/vite-env.d.ts +1 -0
  129. package/llms.txt +2 -1
  130. 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
+ }