@alaarab/ogrid-react 2.0.23 → 2.1.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 (66) hide show
  1. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -2
  2. package/dist/esm/components/MarchingAntsOverlay.js +2 -3
  3. package/dist/esm/components/SideBar.js +8 -7
  4. package/dist/esm/components/createOGrid.js +1 -4
  5. package/dist/esm/hooks/index.js +10 -0
  6. package/dist/esm/hooks/useActiveCell.js +2 -4
  7. package/dist/esm/hooks/useCellSelection.js +85 -52
  8. package/dist/esm/hooks/useClipboard.js +15 -54
  9. package/dist/esm/hooks/useColumnChooserState.js +25 -13
  10. package/dist/esm/hooks/useColumnHeaderFilterState.js +22 -11
  11. package/dist/esm/hooks/useColumnHeaderMenuState.js +1 -1
  12. package/dist/esm/hooks/useColumnMeta.js +61 -0
  13. package/dist/esm/hooks/useColumnPinning.js +11 -12
  14. package/dist/esm/hooks/useColumnReorder.js +8 -1
  15. package/dist/esm/hooks/useColumnResize.js +6 -2
  16. package/dist/esm/hooks/useDataGridContextMenu.js +24 -0
  17. package/dist/esm/hooks/useDataGridEditing.js +56 -0
  18. package/dist/esm/hooks/useDataGridInteraction.js +109 -0
  19. package/dist/esm/hooks/useDataGridLayout.js +172 -0
  20. package/dist/esm/hooks/useDataGridState.js +83 -318
  21. package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -4
  22. package/dist/esm/hooks/useFillHandle.js +60 -55
  23. package/dist/esm/hooks/useFilterOptions.js +21 -6
  24. package/dist/esm/hooks/useInlineCellEditorState.js +7 -13
  25. package/dist/esm/hooks/useKeyboardNavigation.js +19 -132
  26. package/dist/esm/hooks/useMultiSelectFilterState.js +1 -1
  27. package/dist/esm/hooks/useOGrid.js +158 -301
  28. package/dist/esm/hooks/useOGridDataFetching.js +74 -0
  29. package/dist/esm/hooks/useOGridFilters.js +59 -0
  30. package/dist/esm/hooks/useOGridPagination.js +24 -0
  31. package/dist/esm/hooks/useOGridSorting.js +24 -0
  32. package/dist/esm/hooks/usePaginationControls.js +2 -5
  33. package/dist/esm/hooks/usePeopleFilterState.js +6 -1
  34. package/dist/esm/hooks/useRichSelectState.js +7 -5
  35. package/dist/esm/hooks/useRowSelection.js +6 -26
  36. package/dist/esm/hooks/useSelectState.js +2 -5
  37. package/dist/esm/hooks/useShallowEqualMemo.js +14 -0
  38. package/dist/esm/hooks/useTableLayout.js +3 -11
  39. package/dist/esm/hooks/useUndoRedo.js +16 -10
  40. package/dist/esm/index.js +1 -1
  41. package/dist/esm/utils/index.js +1 -1
  42. package/dist/types/components/ColumnChooserProps.d.ts +2 -0
  43. package/dist/types/components/ColumnHeaderFilterContent.d.ts +0 -2
  44. package/dist/types/hooks/index.d.ts +19 -0
  45. package/dist/types/hooks/useClipboard.d.ts +0 -1
  46. package/dist/types/hooks/useColumnChooserState.d.ts +2 -0
  47. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -2
  48. package/dist/types/hooks/useColumnHeaderMenuState.d.ts +0 -2
  49. package/dist/types/hooks/useColumnMeta.d.ts +34 -0
  50. package/dist/types/hooks/useDataGridContextMenu.d.ts +20 -0
  51. package/dist/types/hooks/useDataGridEditing.d.ts +39 -0
  52. package/dist/types/hooks/useDataGridInteraction.d.ts +95 -0
  53. package/dist/types/hooks/useDataGridLayout.d.ts +45 -0
  54. package/dist/types/hooks/useDataGridState.d.ts +7 -1
  55. package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -2
  56. package/dist/types/hooks/useOGrid.d.ts +4 -2
  57. package/dist/types/hooks/useOGridDataFetching.d.ts +28 -0
  58. package/dist/types/hooks/useOGridFilters.d.ts +24 -0
  59. package/dist/types/hooks/useOGridPagination.d.ts +18 -0
  60. package/dist/types/hooks/useOGridSorting.d.ts +23 -0
  61. package/dist/types/hooks/usePaginationControls.d.ts +1 -1
  62. package/dist/types/hooks/useRichSelectState.d.ts +2 -0
  63. package/dist/types/hooks/useShallowEqualMemo.d.ts +7 -0
  64. package/dist/types/index.d.ts +2 -2
  65. package/dist/types/utils/index.d.ts +2 -2
  66. package/package.json +12 -4
@@ -1,84 +1,84 @@
1
- import { useMemo, useCallback, useState, useEffect, useRef, useImperativeHandle, } from 'react';
2
- import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, computeNextSortState, } from '../utils';
3
- import { useFilterOptions } from './useFilterOptions';
1
+ import { useMemo, useCallback, useState, useImperativeHandle, useEffect, useRef, } from 'react';
2
+ import { flattenColumns } from '../utils';
3
+ import { validateColumns, validateRowIds } from '@alaarab/ogrid-core';
4
+ import { useOGridPagination } from './useOGridPagination';
5
+ import { useOGridSorting } from './useOGridSorting';
6
+ import { useOGridFilters } from './useOGridFilters';
7
+ import { useOGridDataFetching } from './useOGridDataFetching';
8
+ import { useLatestRef } from './useLatestRef';
4
9
  import { useSideBarState } from './useSideBarState';
5
10
  const DEFAULT_PAGE_SIZE = 25;
6
11
  const EMPTY_LOADING_OPTIONS = {};
7
12
  /**
8
13
  * Top-level orchestration hook for OGrid: manages pagination, sorting, filtering, column visibility, and sidebar.
14
+ * Delegates to focused sub-hooks for each concern.
9
15
  * @param props - All OGrid props (columns, data, callbacks, feature flags).
10
16
  * @param ref - Forwarded ref for imperative API (refresh, export, applyColumnState).
11
17
  * @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
12
18
  */
13
19
  export function useOGrid(props, ref) {
14
20
  const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, rowHeight, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
15
- // Resolve column chooser placement
21
+ // --- Derived column state ---
16
22
  const columnChooserPlacement = columnChooserProp === false ? 'none'
17
23
  : columnChooserProp === 'sidebar' ? 'sidebar'
18
24
  : 'toolbar';
19
25
  const columns = useMemo(() => flattenColumns(columnsProp), [columnsProp]);
20
26
  const isServerSide = dataSource != null;
21
- const isClientSide = !isServerSide;
27
+ // --- Runtime validation (dev-only, runs once on mount) ---
28
+ const rowIdsValidatedRef = useRef(false);
29
+ useEffect(() => {
30
+ validateColumns(columns);
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, []); // intentionally empty — run once at mount
33
+ const defaultSortField = defaultSortBy ?? columns[0]?.columnId ?? '';
34
+ // --- Internal data state (for imperative setRowData/setLoading API) ---
22
35
  const [internalData, setInternalData] = useState([]);
23
36
  const [internalLoading, setInternalLoading] = useState(false);
24
37
  const displayData = data ?? internalData;
25
38
  const displayLoading = controlledLoading ?? internalLoading;
26
- const defaultSortField = defaultSortBy ?? columns[0]?.columnId ?? '';
27
- const [internalPage, setInternalPage] = useState(1);
28
- const [internalPageSize, setInternalPageSize] = useState(defaultPageSize);
29
- const [internalSort, setInternalSort] = useState({
30
- field: defaultSortField,
31
- direction: defaultSortDirection,
39
+ // --- Sub-hooks ---
40
+ const paginationState = useOGridPagination({
41
+ controlledPage, controlledPageSize, defaultPageSize,
42
+ onPageChange, onPageSizeChange,
43
+ });
44
+ const sortingState = useOGridSorting({
45
+ controlledSort, defaultSortField, defaultSortDirection,
46
+ onSortChange, setPage: paginationState.setPage,
47
+ });
48
+ const filtersState = useOGridFilters({
49
+ controlledFilters, onFiltersChange,
50
+ setPage: paginationState.setPage,
51
+ columns, displayData, dataSource,
32
52
  });
33
- const [internalFilters, setInternalFilters] = useState({});
53
+ const dataFetchingState = useOGridDataFetching({
54
+ isServerSide, dataSource, displayData, columns,
55
+ stableFilters: filtersState.stableFilters,
56
+ sort: sortingState.sort,
57
+ page: paginationState.page,
58
+ pageSize: paginationState.pageSize,
59
+ onError, onFirstDataRendered,
60
+ });
61
+ // Validate row IDs once on first data render
62
+ useEffect(() => {
63
+ const items = dataFetchingState.displayItems;
64
+ if (!rowIdsValidatedRef.current && items.length > 0) {
65
+ rowIdsValidatedRef.current = true;
66
+ validateRowIds(items, getRowId);
67
+ }
68
+ }, [dataFetchingState.displayItems, getRowId]);
69
+ // --- Column visibility ---
34
70
  const [internalVisibleColumns, setInternalVisibleColumns] = useState(() => {
35
71
  const visible = columns
36
72
  .filter((c) => c.defaultVisible !== false)
37
73
  .map((c) => c.columnId);
38
74
  return new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId));
39
75
  });
40
- const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
41
- const [pinnedOverrides, setPinnedOverrides] = useState({});
42
- const page = controlledPage ?? internalPage;
43
- const pageSize = controlledPageSize ?? internalPageSize;
44
- const sort = controlledSort ?? internalSort;
45
- const filters = controlledFilters ?? internalFilters;
46
76
  const visibleColumns = controlledVisibleColumns ?? internalVisibleColumns;
47
- const setPage = useCallback((p) => {
48
- if (controlledPage === undefined)
49
- setInternalPage(p);
50
- onPageChange?.(p);
51
- }, [controlledPage, onPageChange]);
52
- const setPageSize = useCallback((size) => {
53
- if (controlledPageSize === undefined)
54
- setInternalPageSize(size);
55
- onPageSizeChange?.(size);
56
- setPage(1);
57
- }, [controlledPageSize, onPageSizeChange, setPage]);
58
- const setSort = useCallback((s) => {
59
- if (controlledSort === undefined)
60
- setInternalSort(s);
61
- onSortChange?.(s);
62
- setPage(1);
63
- }, [controlledSort, onSortChange, setPage]);
64
- const setFilters = useCallback((f) => {
65
- if (controlledFilters === undefined)
66
- setInternalFilters(f);
67
- onFiltersChange?.(f);
68
- setPage(1);
69
- }, [controlledFilters, onFiltersChange, setPage]);
70
77
  const setVisibleColumns = useCallback((cols) => {
71
78
  if (controlledVisibleColumns === undefined)
72
79
  setInternalVisibleColumns(cols);
73
80
  onVisibleColumnsChange?.(cols);
74
81
  }, [controlledVisibleColumns, onVisibleColumnsChange]);
75
- const handleSort = useCallback((columnKey, direction) => {
76
- setSort(computeNextSortState(sort, columnKey, direction));
77
- }, [sort, setSort]);
78
- /** Single filter change handler — wraps discriminated FilterValue into mergeFilter. */
79
- const handleFilterChange = useCallback((key, value) => {
80
- setFilters(mergeFilter(filters, key, value));
81
- }, [filters, setFilters]);
82
82
  const handleVisibilityChange = useCallback((columnKey, isVisible) => {
83
83
  const next = new Set(visibleColumns);
84
84
  if (isVisible)
@@ -87,6 +87,7 @@ export function useOGrid(props, ref) {
87
87
  next.delete(columnKey);
88
88
  setVisibleColumns(next);
89
89
  }, [visibleColumns, setVisibleColumns]);
90
+ // --- Row selection ---
90
91
  const [internalSelectedRows, setInternalSelectedRows] = useState(new Set());
91
92
  const effectiveSelectedRows = selectedRows ?? internalSelectedRows;
92
93
  const handleSelectionChange = useCallback((event) => {
@@ -95,117 +96,34 @@ export function useOGrid(props, ref) {
95
96
  }
96
97
  onSelectionChange?.(event);
97
98
  }, [selectedRows, onSelectionChange]);
98
- const multiSelectFilterFields = useMemo(() => getMultiSelectFilterFields(columns), [columns]);
99
- const filterOptionsSource = useMemo(() => dataSource ?? { fetchFilterOptions: undefined }, [dataSource]);
100
- const { filterOptions: serverFilterOptions, loadingOptions: loadingFilterOptions } = useFilterOptions(filterOptionsSource, multiSelectFilterFields);
101
- const hasServerFilterOptions = dataSource?.fetchFilterOptions != null;
102
- const clientFilterOptions = useMemo(() => {
103
- if (hasServerFilterOptions)
104
- return serverFilterOptions;
105
- return deriveFilterOptionsFromData(displayData, columns);
106
- }, [hasServerFilterOptions, displayData, columns, serverFilterOptions]);
107
- // --- Client-side filtering & sorting ---
108
- // Stabilize filters ref via shallow comparison so processClientSideData useMemo
109
- // doesn't re-run when the filter object reference changes but values are identical.
110
- const stableFiltersRef = useRef(filters);
111
- const stableFilters = useMemo(() => {
112
- const prev = stableFiltersRef.current;
113
- const prevKeys = Object.keys(prev);
114
- const nextKeys = Object.keys(filters);
115
- if (prevKeys.length !== nextKeys.length) {
116
- stableFiltersRef.current = filters;
117
- return filters;
118
- }
119
- for (let i = 0; i < nextKeys.length; i++) {
120
- if (prev[nextKeys[i]] !== filters[nextKeys[i]]) {
121
- stableFiltersRef.current = filters;
122
- return filters;
99
+ // --- Column resize & pin ---
100
+ const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
101
+ const [pinnedOverrides, setPinnedOverrides] = useState({});
102
+ const handleColumnResized = useCallback((columnId, width) => {
103
+ setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
104
+ onColumnResized?.(columnId, width);
105
+ }, [onColumnResized]);
106
+ const handleColumnPinned = useCallback((columnId, pinned) => {
107
+ setPinnedOverrides((prev) => {
108
+ if (pinned === null) {
109
+ const { [columnId]: _, ...rest } = prev;
110
+ return rest;
123
111
  }
124
- }
125
- return prev;
126
- }, [filters]);
127
- const clientItemsAndTotal = useMemo(() => {
128
- if (!isClientSide)
129
- return null;
130
- const rows = processClientSideData(displayData, columns, stableFilters, sort.field, sort.direction);
131
- const total = rows.length;
132
- const start = (page - 1) * pageSize;
133
- const paged = rows.slice(start, start + pageSize);
134
- return { items: paged, totalCount: total };
135
- }, [
136
- isClientSide,
137
- displayData,
138
- columns,
139
- stableFilters,
140
- sort.field,
141
- sort.direction,
142
- page,
143
- pageSize,
144
- ]);
145
- const [serverItems, setServerItems] = useState([]);
146
- const [serverTotalCount, setServerTotalCount] = useState(0);
147
- const [loading, setLoading] = useState(true);
148
- const fetchIdRef = useRef(0);
149
- // Ref counter to trigger server-side re-fetches
150
- const refreshCounterRef = useRef(0);
151
- const [refreshCounter, setRefreshCounter] = useState(0);
152
- useEffect(() => {
153
- if (!isServerSide || !dataSource) {
154
- if (!isServerSide)
155
- setLoading(false);
156
- return;
157
- }
158
- const id = ++fetchIdRef.current;
159
- setLoading(true);
160
- dataSource
161
- .fetchPage({
162
- page,
163
- pageSize,
164
- sort: { field: sort.field, direction: sort.direction },
165
- filters,
166
- })
167
- .then((res) => {
168
- if (id !== fetchIdRef.current)
169
- return;
170
- setServerItems(res.items);
171
- setServerTotalCount(res.totalCount);
172
- })
173
- .catch((err) => {
174
- if (id !== fetchIdRef.current)
175
- return;
176
- onError?.(err);
177
- setServerItems([]);
178
- setServerTotalCount(0);
179
- })
180
- .finally(() => {
181
- if (id === fetchIdRef.current)
182
- setLoading(false);
112
+ return { ...prev, [columnId]: pinned };
183
113
  });
184
- }, [
185
- isServerSide,
186
- dataSource,
187
- page,
188
- pageSize,
189
- sort.field,
190
- sort.direction,
191
- filters,
192
- onError,
193
- refreshCounter,
194
- ]);
195
- const displayItems = isClientSide && clientItemsAndTotal
196
- ? clientItemsAndTotal.items
197
- : serverItems;
198
- const displayTotalCount = isClientSide && clientItemsAndTotal
199
- ? clientItemsAndTotal.totalCount
200
- : serverTotalCount;
201
- // Fire onFirstDataRendered once when the grid first has data
202
- const firstDataRenderedRef = useRef(false);
203
- useEffect(() => {
204
- if (!firstDataRenderedRef.current && displayItems.length > 0) {
205
- firstDataRenderedRef.current = true;
206
- onFirstDataRendered?.();
207
- }
208
- }, [displayItems.length, onFirstDataRendered]);
114
+ onColumnPinned?.(columnId, pinned);
115
+ }, [onColumnPinned]);
116
+ // --- Imperative handle (stabilized via refs to avoid invalidation on every state change) ---
117
+ const visibleColumnsRef = useLatestRef(visibleColumns);
118
+ const sortRef = useLatestRef(sortingState.sort);
119
+ const columnOrderRef = useLatestRef(columnOrder);
120
+ const columnWidthOverridesRef = useLatestRef(columnWidthOverrides);
121
+ const pinnedOverridesRef = useLatestRef(pinnedOverrides);
122
+ const filtersRef = useLatestRef(filtersState.filters);
123
+ const effectiveSelectedRowsRef = useLatestRef(effectiveSelectedRows);
124
+ const displayItemsRef = useLatestRef(dataFetchingState.displayItems);
125
+ const getRowIdRef = useLatestRef(getRowId);
126
+ const columnsRef = useLatestRef(columns);
209
127
  useImperativeHandle(ref, () => ({
210
128
  setRowData: (d) => {
211
129
  if (!isServerSide)
@@ -213,75 +131,62 @@ export function useOGrid(props, ref) {
213
131
  },
214
132
  setLoading: setInternalLoading,
215
133
  getColumnState: () => ({
216
- visibleColumns: Array.from(visibleColumns),
217
- sort,
218
- columnOrder: columnOrder ?? undefined,
219
- columnWidths: Object.keys(columnWidthOverrides).length > 0 ? columnWidthOverrides : undefined,
220
- filters: Object.keys(filters).length > 0 ? filters : undefined,
221
- pinnedColumns: Object.keys(pinnedOverrides).length > 0 ? pinnedOverrides : undefined,
134
+ visibleColumns: Array.from(visibleColumnsRef.current),
135
+ sort: sortRef.current,
136
+ columnOrder: columnOrderRef.current ?? undefined,
137
+ columnWidths: Object.keys(columnWidthOverridesRef.current).length > 0 ? columnWidthOverridesRef.current : undefined,
138
+ filters: Object.keys(filtersRef.current).length > 0 ? filtersRef.current : undefined,
139
+ pinnedColumns: Object.keys(pinnedOverridesRef.current).length > 0 ? pinnedOverridesRef.current : undefined,
222
140
  }),
223
141
  applyColumnState: (state) => {
224
- if (state.visibleColumns) {
142
+ if (state.visibleColumns)
225
143
  setVisibleColumns(new Set(state.visibleColumns));
226
- }
227
- if (state.sort) {
228
- setSort(state.sort);
229
- }
230
- if (state.columnOrder && onColumnOrderChange) {
144
+ if (state.sort)
145
+ sortingState.setSort(state.sort);
146
+ if (state.columnOrder && onColumnOrderChange)
231
147
  onColumnOrderChange(state.columnOrder);
232
- }
233
- if (state.columnWidths) {
148
+ if (state.columnWidths)
234
149
  setColumnWidthOverrides(state.columnWidths);
235
- }
236
- if (state.filters) {
237
- setFilters(state.filters);
238
- }
239
- if (state.pinnedColumns) {
150
+ if (state.filters)
151
+ filtersState.setFilters(state.filters);
152
+ if (state.pinnedColumns)
240
153
  setPinnedOverrides(state.pinnedColumns);
241
- }
242
154
  },
243
- setFilterModel: setFilters,
244
- getSelectedRows: () => Array.from(effectiveSelectedRows),
155
+ setFilterModel: filtersState.setFilters,
156
+ getSelectedRows: () => Array.from(effectiveSelectedRowsRef.current),
245
157
  setSelectedRows: (rowIds) => {
246
158
  if (selectedRows === undefined)
247
159
  setInternalSelectedRows(new Set(rowIds));
248
160
  },
249
161
  selectAll: () => {
250
- const allIds = new Set(displayItems.map((item) => getRowId(item)));
162
+ const items = displayItemsRef.current;
163
+ const allIds = new Set(items.map((item) => getRowIdRef.current(item)));
251
164
  if (selectedRows === undefined)
252
165
  setInternalSelectedRows(allIds);
253
- onSelectionChange?.({
254
- selectedRowIds: Array.from(allIds),
255
- selectedItems: displayItems,
256
- });
166
+ onSelectionChange?.({ selectedRowIds: Array.from(allIds), selectedItems: items });
257
167
  },
258
168
  deselectAll: () => {
259
169
  if (selectedRows === undefined)
260
170
  setInternalSelectedRows(new Set());
261
- onSelectionChange?.({
262
- selectedRowIds: [],
263
- selectedItems: [],
264
- });
171
+ onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
265
172
  },
266
- clearFilters: () => setFilters({}),
267
- clearSort: () => setSort({ field: defaultSortField, direction: defaultSortDirection }),
173
+ clearFilters: () => filtersState.setFilters({}),
174
+ clearSort: () => sortingState.setSort({ field: sortingState.defaultSortField, direction: sortingState.defaultSortDirection }),
268
175
  resetGridState: (options) => {
269
- setFilters({});
270
- setSort({ field: defaultSortField, direction: defaultSortDirection });
176
+ filtersState.setFilters({});
177
+ sortingState.setSort({ field: sortingState.defaultSortField, direction: sortingState.defaultSortDirection });
271
178
  if (!options?.keepSelection) {
272
179
  if (selectedRows === undefined)
273
180
  setInternalSelectedRows(new Set());
274
181
  onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
275
182
  }
276
183
  },
277
- getDisplayedRows: () => displayItems,
184
+ getDisplayedRows: () => displayItemsRef.current,
278
185
  refreshData: () => {
279
- if (isServerSide) {
280
- refreshCounterRef.current += 1;
281
- setRefreshCounter(refreshCounterRef.current);
282
- }
186
+ if (isServerSide)
187
+ dataFetchingState.refreshData();
283
188
  },
284
- getColumnOrder: () => columnOrder ?? columns.map((c) => c.columnId),
189
+ getColumnOrder: () => columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId),
285
190
  setColumnOrder: (order) => {
286
191
  onColumnOrderChange?.(order);
287
192
  },
@@ -289,82 +194,38 @@ export function useOGrid(props, ref) {
289
194
  // No-op at orchestration level — DataGridTable components implement
290
195
  // this via useVirtualScroll.scrollToIndex when virtual scrolling is active.
291
196
  },
292
- }),
293
- // eslint-disable-next-line react-hooks/exhaustive-deps
294
- [
295
- visibleColumns,
296
- sort,
297
- columnOrder,
298
- columnWidthOverrides,
299
- pinnedOverrides,
300
- filters,
301
- setFilters,
302
- setSort,
303
- setVisibleColumns,
304
- onColumnOrderChange,
305
- isServerSide,
306
- effectiveSelectedRows,
307
- selectedRows,
308
- displayItems,
309
- getRowId,
310
- onSelectionChange,
311
- defaultSortField,
312
- defaultSortDirection,
197
+ }), [
198
+ isServerSide, setVisibleColumns, sortingState, filtersState,
199
+ onColumnOrderChange, selectedRows, onSelectionChange, dataFetchingState,
200
+ columnOrderRef, columnWidthOverridesRef, columnsRef, displayItemsRef,
201
+ effectiveSelectedRowsRef, filtersRef, getRowIdRef, pinnedOverridesRef,
202
+ sortRef, visibleColumnsRef,
313
203
  ]);
314
- // With discriminated union, any defined value is active (mergeFilter already strips empties)
315
- const hasActiveFilters = useMemo(() => {
316
- return Object.values(filters).some((v) => v !== undefined);
317
- }, [filters]);
318
- const columnChooserColumns = useMemo(() => columns.map((c) => ({
319
- columnId: c.columnId,
320
- name: c.name,
321
- required: c.required === true,
322
- })), [columns]);
204
+ // --- Status bar ---
323
205
  const statusBarConfig = useMemo(() => {
324
206
  if (!statusBar)
325
207
  return undefined;
326
208
  if (typeof statusBar === 'object')
327
209
  return statusBar;
328
- const totalData = isClientSide ? (data?.length ?? 0) : serverTotalCount;
329
- const filteredData = displayTotalCount;
210
+ const totalData = !isServerSide ? (data?.length ?? 0) : dataFetchingState.displayTotalCount;
211
+ const filteredData = dataFetchingState.displayTotalCount;
330
212
  return {
331
213
  totalCount: totalData,
332
- filteredCount: hasActiveFilters ? filteredData : undefined,
214
+ filteredCount: filtersState.hasActiveFilters ? filteredData : undefined,
333
215
  selectedCount: effectiveSelectedRows.size,
334
- suppressRowCount: true, // OGrid always has pagination which shows the total
216
+ suppressRowCount: true,
335
217
  };
336
- }, [
337
- statusBar,
338
- isClientSide,
339
- data,
340
- serverTotalCount,
341
- displayTotalCount,
342
- hasActiveFilters,
343
- effectiveSelectedRows.size,
344
- ]);
345
- const handleColumnResized = useCallback((columnId, width) => {
346
- setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
347
- onColumnResized?.(columnId, width);
348
- }, [onColumnResized]);
349
- const handleColumnPinned = useCallback((columnId, pinned) => {
350
- setPinnedOverrides((prev) => {
351
- if (pinned === null) {
352
- const { [columnId]: _, ...rest } = prev;
353
- return rest;
354
- }
355
- return { ...prev, [columnId]: pinned };
356
- });
357
- onColumnPinned?.(columnId, pinned);
358
- }, [onColumnPinned]);
218
+ }, [statusBar, isServerSide, data, dataFetchingState.displayTotalCount, filtersState.hasActiveFilters, effectiveSelectedRows.size]);
359
219
  // --- Side bar ---
360
220
  const sideBarState = useSideBarState({ config: sideBar });
221
+ const columnChooserColumns = useMemo(() => columns.map((c) => ({ columnId: c.columnId, name: c.name, required: c.required === true })), [columns]);
361
222
  const filterableColumns = useMemo(() => columns
362
223
  .filter((c) => c.filterable && c.filterable.type)
363
224
  .map((c) => ({
364
225
  columnId: c.columnId,
365
226
  name: c.name,
366
- filterField: c.filterable.filterField ?? c.columnId,
367
- filterType: c.filterable.type,
227
+ filterField: c.filterable?.filterField ?? c.columnId,
228
+ filterType: c.filterable?.type,
368
229
  })), [columns]);
369
230
  const sideBarProps = useMemo(() => {
370
231
  if (!sideBarState.isEnabled)
@@ -379,34 +240,26 @@ export function useOGrid(props, ref) {
379
240
  onVisibilityChange: handleVisibilityChange,
380
241
  onSetVisibleColumns: setVisibleColumns,
381
242
  filterableColumns,
382
- filters,
383
- onFilterChange: handleFilterChange,
384
- filterOptions: clientFilterOptions,
243
+ filters: filtersState.filters,
244
+ onFilterChange: filtersState.handleFilterChange,
245
+ filterOptions: filtersState.clientFilterOptions,
385
246
  };
386
247
  }, [
387
- sideBarState.isEnabled,
388
- sideBarState.activePanel,
389
- sideBarState.setActivePanel,
390
- sideBarState.panels,
391
- sideBarState.position,
392
- columnChooserColumns,
393
- visibleColumns,
394
- handleVisibilityChange,
395
- setVisibleColumns,
396
- filterableColumns,
397
- filters,
398
- handleFilterChange,
399
- clientFilterOptions,
248
+ sideBarState.isEnabled, sideBarState.activePanel, sideBarState.setActivePanel,
249
+ sideBarState.panels, sideBarState.position,
250
+ columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns,
251
+ filterableColumns, filtersState.filters, filtersState.handleFilterChange, filtersState.clientFilterOptions,
400
252
  ]);
401
- const clearAllFilters = useCallback(() => setFilters({}), [setFilters]);
402
- const isLoadingResolved = (isServerSide && loading) || displayLoading;
253
+ // --- Assembly ---
254
+ const clearAllFilters = useCallback(() => filtersState.setFilters({}), [filtersState]);
255
+ const isLoadingResolved = (isServerSide && dataFetchingState.serverLoading) || displayLoading;
403
256
  const dataGridProps = useMemo(() => ({
404
- items: displayItems,
257
+ items: dataFetchingState.displayItems,
405
258
  columns: columnsProp,
406
259
  getRowId,
407
- sortBy: sort.field,
408
- sortDirection: sort.direction,
409
- onColumnSort: handleSort,
260
+ sortBy: sortingState.sort.field,
261
+ sortDirection: sortingState.sort.direction,
262
+ onColumnSort: sortingState.handleSort,
410
263
  visibleColumns,
411
264
  columnOrder,
412
265
  onColumnOrderChange,
@@ -425,14 +278,14 @@ export function useOGrid(props, ref) {
425
278
  selectedRows: effectiveSelectedRows,
426
279
  onSelectionChange: handleSelectionChange,
427
280
  showRowNumbers,
428
- currentPage: page,
429
- pageSize,
281
+ currentPage: paginationState.page,
282
+ pageSize: paginationState.pageSize,
430
283
  statusBar: statusBarConfig,
431
284
  isLoading: isLoadingResolved,
432
- filters,
433
- onFilterChange: handleFilterChange,
434
- filterOptions: clientFilterOptions,
435
- loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : EMPTY_LOADING_OPTIONS,
285
+ filters: filtersState.filters,
286
+ onFilterChange: filtersState.handleFilterChange,
287
+ filterOptions: filtersState.clientFilterOptions,
288
+ loadingFilterOptions: dataSource?.fetchFilterOptions ? filtersState.loadingFilterOptions : EMPTY_LOADING_OPTIONS,
436
289
  peopleSearch: dataSource?.searchPeople,
437
290
  getUserByEmail: dataSource?.getUserByEmail,
438
291
  layoutMode,
@@ -444,37 +297,41 @@ export function useOGrid(props, ref) {
444
297
  'aria-label': ariaLabel,
445
298
  'aria-labelledby': ariaLabelledBy,
446
299
  emptyState: {
447
- hasActiveFilters,
300
+ hasActiveFilters: filtersState.hasActiveFilters,
448
301
  onClearAll: clearAllFilters,
449
302
  message: emptyState?.message,
450
303
  render: emptyState?.render,
451
304
  },
452
305
  }), [
453
- displayItems, columnsProp, getRowId, sort.field, sort.direction, handleSort,
306
+ dataFetchingState.displayItems, columnsProp, getRowId,
307
+ sortingState.sort.field, sortingState.sort.direction, sortingState.handleSort,
454
308
  visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
455
309
  handleColumnPinned, pinnedOverrides, columnWidthOverrides,
456
310
  editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo,
457
- rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers, page, pageSize, statusBarConfig,
458
- isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
459
- loadingFilterOptions, layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
311
+ rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers,
312
+ paginationState.page, paginationState.pageSize, statusBarConfig,
313
+ isLoadingResolved, filtersState.filters, filtersState.handleFilterChange,
314
+ filtersState.clientFilterOptions, dataSource, filtersState.loadingFilterOptions,
315
+ layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
460
316
  rowHeight, density, ariaLabel, ariaLabelledBy,
461
- hasActiveFilters, clearAllFilters, emptyState,
317
+ filtersState.hasActiveFilters, clearAllFilters, emptyState,
462
318
  ]);
463
319
  const pagination = useMemo(() => ({
464
- page,
465
- pageSize,
466
- displayTotalCount,
467
- setPage,
468
- setPageSize,
320
+ page: paginationState.page,
321
+ pageSize: paginationState.pageSize,
322
+ displayTotalCount: dataFetchingState.displayTotalCount,
323
+ setPage: paginationState.setPage,
324
+ setPageSize: paginationState.setPageSize,
469
325
  pageSizeOptions,
470
326
  entityLabelPlural,
471
- }), [page, pageSize, displayTotalCount, setPage, setPageSize, pageSizeOptions, entityLabelPlural]);
327
+ }), [paginationState.page, paginationState.pageSize, dataFetchingState.displayTotalCount, paginationState.setPage, paginationState.setPageSize, pageSizeOptions, entityLabelPlural]);
472
328
  const columnChooser = useMemo(() => ({
473
329
  columns: columnChooserColumns,
474
330
  visibleColumns,
475
331
  onVisibilityChange: handleVisibilityChange,
332
+ onSetVisibleColumns: setVisibleColumns,
476
333
  placement: columnChooserPlacement,
477
- }), [columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement]);
334
+ }), [columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns, columnChooserPlacement]);
478
335
  const layout = useMemo(() => ({
479
336
  toolbar,
480
337
  toolbarBelow,
@@ -483,9 +340,9 @@ export function useOGrid(props, ref) {
483
340
  sideBarProps,
484
341
  }), [toolbar, toolbarBelow, className, emptyState, sideBarProps]);
485
342
  const filtersResult = useMemo(() => ({
486
- hasActiveFilters,
487
- setFilters,
488
- }), [hasActiveFilters, setFilters]);
343
+ hasActiveFilters: filtersState.hasActiveFilters,
344
+ setFilters: filtersState.setFilters,
345
+ }), [filtersState.hasActiveFilters, filtersState.setFilters]);
489
346
  return {
490
347
  dataGridProps,
491
348
  pagination,