@alaarab/ogrid-react 2.0.23 → 2.1.0

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 +2 -4
  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 +159 -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 +29 -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,85 @@
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
+ filters: filtersState.filters,
57
+ sort: sortingState.sort,
58
+ page: paginationState.page,
59
+ pageSize: paginationState.pageSize,
60
+ onError, onFirstDataRendered,
61
+ });
62
+ // Validate row IDs once on first data render
63
+ useEffect(() => {
64
+ const items = dataFetchingState.displayItems;
65
+ if (!rowIdsValidatedRef.current && items.length > 0) {
66
+ rowIdsValidatedRef.current = true;
67
+ validateRowIds(items, getRowId);
68
+ }
69
+ }, [dataFetchingState.displayItems, getRowId]);
70
+ // --- Column visibility ---
34
71
  const [internalVisibleColumns, setInternalVisibleColumns] = useState(() => {
35
72
  const visible = columns
36
73
  .filter((c) => c.defaultVisible !== false)
37
74
  .map((c) => c.columnId);
38
75
  return new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId));
39
76
  });
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
77
  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
78
  const setVisibleColumns = useCallback((cols) => {
71
79
  if (controlledVisibleColumns === undefined)
72
80
  setInternalVisibleColumns(cols);
73
81
  onVisibleColumnsChange?.(cols);
74
82
  }, [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
83
  const handleVisibilityChange = useCallback((columnKey, isVisible) => {
83
84
  const next = new Set(visibleColumns);
84
85
  if (isVisible)
@@ -87,6 +88,7 @@ export function useOGrid(props, ref) {
87
88
  next.delete(columnKey);
88
89
  setVisibleColumns(next);
89
90
  }, [visibleColumns, setVisibleColumns]);
91
+ // --- Row selection ---
90
92
  const [internalSelectedRows, setInternalSelectedRows] = useState(new Set());
91
93
  const effectiveSelectedRows = selectedRows ?? internalSelectedRows;
92
94
  const handleSelectionChange = useCallback((event) => {
@@ -95,117 +97,34 @@ export function useOGrid(props, ref) {
95
97
  }
96
98
  onSelectionChange?.(event);
97
99
  }, [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;
100
+ // --- Column resize & pin ---
101
+ const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
102
+ const [pinnedOverrides, setPinnedOverrides] = useState({});
103
+ const handleColumnResized = useCallback((columnId, width) => {
104
+ setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
105
+ onColumnResized?.(columnId, width);
106
+ }, [onColumnResized]);
107
+ const handleColumnPinned = useCallback((columnId, pinned) => {
108
+ setPinnedOverrides((prev) => {
109
+ if (pinned === null) {
110
+ const { [columnId]: _, ...rest } = prev;
111
+ return rest;
123
112
  }
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);
113
+ return { ...prev, [columnId]: pinned };
183
114
  });
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]);
115
+ onColumnPinned?.(columnId, pinned);
116
+ }, [onColumnPinned]);
117
+ // --- Imperative handle (stabilized via refs to avoid invalidation on every state change) ---
118
+ const visibleColumnsRef = useLatestRef(visibleColumns);
119
+ const sortRef = useLatestRef(sortingState.sort);
120
+ const columnOrderRef = useLatestRef(columnOrder);
121
+ const columnWidthOverridesRef = useLatestRef(columnWidthOverrides);
122
+ const pinnedOverridesRef = useLatestRef(pinnedOverrides);
123
+ const filtersRef = useLatestRef(filtersState.filters);
124
+ const effectiveSelectedRowsRef = useLatestRef(effectiveSelectedRows);
125
+ const displayItemsRef = useLatestRef(dataFetchingState.displayItems);
126
+ const getRowIdRef = useLatestRef(getRowId);
127
+ const columnsRef = useLatestRef(columns);
209
128
  useImperativeHandle(ref, () => ({
210
129
  setRowData: (d) => {
211
130
  if (!isServerSide)
@@ -213,75 +132,62 @@ export function useOGrid(props, ref) {
213
132
  },
214
133
  setLoading: setInternalLoading,
215
134
  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,
135
+ visibleColumns: Array.from(visibleColumnsRef.current),
136
+ sort: sortRef.current,
137
+ columnOrder: columnOrderRef.current ?? undefined,
138
+ columnWidths: Object.keys(columnWidthOverridesRef.current).length > 0 ? columnWidthOverridesRef.current : undefined,
139
+ filters: Object.keys(filtersRef.current).length > 0 ? filtersRef.current : undefined,
140
+ pinnedColumns: Object.keys(pinnedOverridesRef.current).length > 0 ? pinnedOverridesRef.current : undefined,
222
141
  }),
223
142
  applyColumnState: (state) => {
224
- if (state.visibleColumns) {
143
+ if (state.visibleColumns)
225
144
  setVisibleColumns(new Set(state.visibleColumns));
226
- }
227
- if (state.sort) {
228
- setSort(state.sort);
229
- }
230
- if (state.columnOrder && onColumnOrderChange) {
145
+ if (state.sort)
146
+ sortingState.setSort(state.sort);
147
+ if (state.columnOrder && onColumnOrderChange)
231
148
  onColumnOrderChange(state.columnOrder);
232
- }
233
- if (state.columnWidths) {
149
+ if (state.columnWidths)
234
150
  setColumnWidthOverrides(state.columnWidths);
235
- }
236
- if (state.filters) {
237
- setFilters(state.filters);
238
- }
239
- if (state.pinnedColumns) {
151
+ if (state.filters)
152
+ filtersState.setFilters(state.filters);
153
+ if (state.pinnedColumns)
240
154
  setPinnedOverrides(state.pinnedColumns);
241
- }
242
155
  },
243
- setFilterModel: setFilters,
244
- getSelectedRows: () => Array.from(effectiveSelectedRows),
156
+ setFilterModel: filtersState.setFilters,
157
+ getSelectedRows: () => Array.from(effectiveSelectedRowsRef.current),
245
158
  setSelectedRows: (rowIds) => {
246
159
  if (selectedRows === undefined)
247
160
  setInternalSelectedRows(new Set(rowIds));
248
161
  },
249
162
  selectAll: () => {
250
- const allIds = new Set(displayItems.map((item) => getRowId(item)));
163
+ const items = displayItemsRef.current;
164
+ const allIds = new Set(items.map((item) => getRowIdRef.current(item)));
251
165
  if (selectedRows === undefined)
252
166
  setInternalSelectedRows(allIds);
253
- onSelectionChange?.({
254
- selectedRowIds: Array.from(allIds),
255
- selectedItems: displayItems,
256
- });
167
+ onSelectionChange?.({ selectedRowIds: Array.from(allIds), selectedItems: items });
257
168
  },
258
169
  deselectAll: () => {
259
170
  if (selectedRows === undefined)
260
171
  setInternalSelectedRows(new Set());
261
- onSelectionChange?.({
262
- selectedRowIds: [],
263
- selectedItems: [],
264
- });
172
+ onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
265
173
  },
266
- clearFilters: () => setFilters({}),
267
- clearSort: () => setSort({ field: defaultSortField, direction: defaultSortDirection }),
174
+ clearFilters: () => filtersState.setFilters({}),
175
+ clearSort: () => sortingState.setSort({ field: sortingState.defaultSortField, direction: sortingState.defaultSortDirection }),
268
176
  resetGridState: (options) => {
269
- setFilters({});
270
- setSort({ field: defaultSortField, direction: defaultSortDirection });
177
+ filtersState.setFilters({});
178
+ sortingState.setSort({ field: sortingState.defaultSortField, direction: sortingState.defaultSortDirection });
271
179
  if (!options?.keepSelection) {
272
180
  if (selectedRows === undefined)
273
181
  setInternalSelectedRows(new Set());
274
182
  onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
275
183
  }
276
184
  },
277
- getDisplayedRows: () => displayItems,
185
+ getDisplayedRows: () => displayItemsRef.current,
278
186
  refreshData: () => {
279
- if (isServerSide) {
280
- refreshCounterRef.current += 1;
281
- setRefreshCounter(refreshCounterRef.current);
282
- }
187
+ if (isServerSide)
188
+ dataFetchingState.refreshData();
283
189
  },
284
- getColumnOrder: () => columnOrder ?? columns.map((c) => c.columnId),
190
+ getColumnOrder: () => columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId),
285
191
  setColumnOrder: (order) => {
286
192
  onColumnOrderChange?.(order);
287
193
  },
@@ -289,82 +195,38 @@ export function useOGrid(props, ref) {
289
195
  // No-op at orchestration level — DataGridTable components implement
290
196
  // this via useVirtualScroll.scrollToIndex when virtual scrolling is active.
291
197
  },
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,
198
+ }), [
199
+ isServerSide, setVisibleColumns, sortingState, filtersState,
200
+ onColumnOrderChange, selectedRows, onSelectionChange, dataFetchingState,
201
+ columnOrderRef, columnWidthOverridesRef, columnsRef, displayItemsRef,
202
+ effectiveSelectedRowsRef, filtersRef, getRowIdRef, pinnedOverridesRef,
203
+ sortRef, visibleColumnsRef,
313
204
  ]);
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]);
205
+ // --- Status bar ---
323
206
  const statusBarConfig = useMemo(() => {
324
207
  if (!statusBar)
325
208
  return undefined;
326
209
  if (typeof statusBar === 'object')
327
210
  return statusBar;
328
- const totalData = isClientSide ? (data?.length ?? 0) : serverTotalCount;
329
- const filteredData = displayTotalCount;
211
+ const totalData = !isServerSide ? (data?.length ?? 0) : dataFetchingState.displayTotalCount;
212
+ const filteredData = dataFetchingState.displayTotalCount;
330
213
  return {
331
214
  totalCount: totalData,
332
- filteredCount: hasActiveFilters ? filteredData : undefined,
215
+ filteredCount: filtersState.hasActiveFilters ? filteredData : undefined,
333
216
  selectedCount: effectiveSelectedRows.size,
334
- suppressRowCount: true, // OGrid always has pagination which shows the total
217
+ suppressRowCount: true,
335
218
  };
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]);
219
+ }, [statusBar, isServerSide, data, dataFetchingState.displayTotalCount, filtersState.hasActiveFilters, effectiveSelectedRows.size]);
359
220
  // --- Side bar ---
360
221
  const sideBarState = useSideBarState({ config: sideBar });
222
+ const columnChooserColumns = useMemo(() => columns.map((c) => ({ columnId: c.columnId, name: c.name, required: c.required === true })), [columns]);
361
223
  const filterableColumns = useMemo(() => columns
362
224
  .filter((c) => c.filterable && c.filterable.type)
363
225
  .map((c) => ({
364
226
  columnId: c.columnId,
365
227
  name: c.name,
366
- filterField: c.filterable.filterField ?? c.columnId,
367
- filterType: c.filterable.type,
228
+ filterField: c.filterable?.filterField ?? c.columnId,
229
+ filterType: c.filterable?.type,
368
230
  })), [columns]);
369
231
  const sideBarProps = useMemo(() => {
370
232
  if (!sideBarState.isEnabled)
@@ -379,34 +241,26 @@ export function useOGrid(props, ref) {
379
241
  onVisibilityChange: handleVisibilityChange,
380
242
  onSetVisibleColumns: setVisibleColumns,
381
243
  filterableColumns,
382
- filters,
383
- onFilterChange: handleFilterChange,
384
- filterOptions: clientFilterOptions,
244
+ filters: filtersState.filters,
245
+ onFilterChange: filtersState.handleFilterChange,
246
+ filterOptions: filtersState.clientFilterOptions,
385
247
  };
386
248
  }, [
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,
249
+ sideBarState.isEnabled, sideBarState.activePanel, sideBarState.setActivePanel,
250
+ sideBarState.panels, sideBarState.position,
251
+ columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns,
252
+ filterableColumns, filtersState.filters, filtersState.handleFilterChange, filtersState.clientFilterOptions,
400
253
  ]);
401
- const clearAllFilters = useCallback(() => setFilters({}), [setFilters]);
402
- const isLoadingResolved = (isServerSide && loading) || displayLoading;
254
+ // --- Assembly ---
255
+ const clearAllFilters = useCallback(() => filtersState.setFilters({}), [filtersState]);
256
+ const isLoadingResolved = (isServerSide && dataFetchingState.serverLoading) || displayLoading;
403
257
  const dataGridProps = useMemo(() => ({
404
- items: displayItems,
258
+ items: dataFetchingState.displayItems,
405
259
  columns: columnsProp,
406
260
  getRowId,
407
- sortBy: sort.field,
408
- sortDirection: sort.direction,
409
- onColumnSort: handleSort,
261
+ sortBy: sortingState.sort.field,
262
+ sortDirection: sortingState.sort.direction,
263
+ onColumnSort: sortingState.handleSort,
410
264
  visibleColumns,
411
265
  columnOrder,
412
266
  onColumnOrderChange,
@@ -425,14 +279,14 @@ export function useOGrid(props, ref) {
425
279
  selectedRows: effectiveSelectedRows,
426
280
  onSelectionChange: handleSelectionChange,
427
281
  showRowNumbers,
428
- currentPage: page,
429
- pageSize,
282
+ currentPage: paginationState.page,
283
+ pageSize: paginationState.pageSize,
430
284
  statusBar: statusBarConfig,
431
285
  isLoading: isLoadingResolved,
432
- filters,
433
- onFilterChange: handleFilterChange,
434
- filterOptions: clientFilterOptions,
435
- loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : EMPTY_LOADING_OPTIONS,
286
+ filters: filtersState.filters,
287
+ onFilterChange: filtersState.handleFilterChange,
288
+ filterOptions: filtersState.clientFilterOptions,
289
+ loadingFilterOptions: dataSource?.fetchFilterOptions ? filtersState.loadingFilterOptions : EMPTY_LOADING_OPTIONS,
436
290
  peopleSearch: dataSource?.searchPeople,
437
291
  getUserByEmail: dataSource?.getUserByEmail,
438
292
  layoutMode,
@@ -444,37 +298,41 @@ export function useOGrid(props, ref) {
444
298
  'aria-label': ariaLabel,
445
299
  'aria-labelledby': ariaLabelledBy,
446
300
  emptyState: {
447
- hasActiveFilters,
301
+ hasActiveFilters: filtersState.hasActiveFilters,
448
302
  onClearAll: clearAllFilters,
449
303
  message: emptyState?.message,
450
304
  render: emptyState?.render,
451
305
  },
452
306
  }), [
453
- displayItems, columnsProp, getRowId, sort.field, sort.direction, handleSort,
307
+ dataFetchingState.displayItems, columnsProp, getRowId,
308
+ sortingState.sort.field, sortingState.sort.direction, sortingState.handleSort,
454
309
  visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
455
310
  handleColumnPinned, pinnedOverrides, columnWidthOverrides,
456
311
  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,
312
+ rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers,
313
+ paginationState.page, paginationState.pageSize, statusBarConfig,
314
+ isLoadingResolved, filtersState.filters, filtersState.handleFilterChange,
315
+ filtersState.clientFilterOptions, dataSource, filtersState.loadingFilterOptions,
316
+ layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
460
317
  rowHeight, density, ariaLabel, ariaLabelledBy,
461
- hasActiveFilters, clearAllFilters, emptyState,
318
+ filtersState.hasActiveFilters, clearAllFilters, emptyState,
462
319
  ]);
463
320
  const pagination = useMemo(() => ({
464
- page,
465
- pageSize,
466
- displayTotalCount,
467
- setPage,
468
- setPageSize,
321
+ page: paginationState.page,
322
+ pageSize: paginationState.pageSize,
323
+ displayTotalCount: dataFetchingState.displayTotalCount,
324
+ setPage: paginationState.setPage,
325
+ setPageSize: paginationState.setPageSize,
469
326
  pageSizeOptions,
470
327
  entityLabelPlural,
471
- }), [page, pageSize, displayTotalCount, setPage, setPageSize, pageSizeOptions, entityLabelPlural]);
328
+ }), [paginationState.page, paginationState.pageSize, dataFetchingState.displayTotalCount, paginationState.setPage, paginationState.setPageSize, pageSizeOptions, entityLabelPlural]);
472
329
  const columnChooser = useMemo(() => ({
473
330
  columns: columnChooserColumns,
474
331
  visibleColumns,
475
332
  onVisibilityChange: handleVisibilityChange,
333
+ onSetVisibleColumns: setVisibleColumns,
476
334
  placement: columnChooserPlacement,
477
- }), [columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement]);
335
+ }), [columnChooserColumns, visibleColumns, handleVisibilityChange, setVisibleColumns, columnChooserPlacement]);
478
336
  const layout = useMemo(() => ({
479
337
  toolbar,
480
338
  toolbarBelow,
@@ -483,9 +341,9 @@ export function useOGrid(props, ref) {
483
341
  sideBarProps,
484
342
  }), [toolbar, toolbarBelow, className, emptyState, sideBarProps]);
485
343
  const filtersResult = useMemo(() => ({
486
- hasActiveFilters,
487
- setFilters,
488
- }), [hasActiveFilters, setFilters]);
344
+ hasActiveFilters: filtersState.hasActiveFilters,
345
+ setFilters: filtersState.setFilters,
346
+ }), [filtersState.hasActiveFilters, filtersState.setFilters]);
489
347
  return {
490
348
  dataGridProps,
491
349
  pagination,