@alaarab/ogrid-react 2.0.0-beta

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 (88) hide show
  1. package/README.md +55 -0
  2. package/dist/esm/components/BaseInlineCellEditor.js +112 -0
  3. package/dist/esm/components/CellErrorBoundary.js +43 -0
  4. package/dist/esm/components/EmptyState.js +19 -0
  5. package/dist/esm/components/GridContextMenu.js +35 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +110 -0
  7. package/dist/esm/components/OGridLayout.js +91 -0
  8. package/dist/esm/components/SideBar.js +122 -0
  9. package/dist/esm/components/StatusBar.js +6 -0
  10. package/dist/esm/hooks/index.js +25 -0
  11. package/dist/esm/hooks/useActiveCell.js +62 -0
  12. package/dist/esm/hooks/useCellEditing.js +15 -0
  13. package/dist/esm/hooks/useCellSelection.js +327 -0
  14. package/dist/esm/hooks/useClipboard.js +161 -0
  15. package/dist/esm/hooks/useColumnChooserState.js +62 -0
  16. package/dist/esm/hooks/useColumnHeaderFilterState.js +180 -0
  17. package/dist/esm/hooks/useColumnResize.js +92 -0
  18. package/dist/esm/hooks/useContextMenu.js +21 -0
  19. package/dist/esm/hooks/useDataGridState.js +313 -0
  20. package/dist/esm/hooks/useDateFilterState.js +34 -0
  21. package/dist/esm/hooks/useDebounce.js +35 -0
  22. package/dist/esm/hooks/useFillHandle.js +195 -0
  23. package/dist/esm/hooks/useFilterOptions.js +40 -0
  24. package/dist/esm/hooks/useInlineCellEditorState.js +44 -0
  25. package/dist/esm/hooks/useKeyboardNavigation.js +419 -0
  26. package/dist/esm/hooks/useLatestRef.js +11 -0
  27. package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
  28. package/dist/esm/hooks/useOGrid.js +465 -0
  29. package/dist/esm/hooks/usePeopleFilterState.js +68 -0
  30. package/dist/esm/hooks/useRichSelectState.js +58 -0
  31. package/dist/esm/hooks/useRowSelection.js +80 -0
  32. package/dist/esm/hooks/useSideBarState.js +39 -0
  33. package/dist/esm/hooks/useTableLayout.js +77 -0
  34. package/dist/esm/hooks/useTextFilterState.js +25 -0
  35. package/dist/esm/hooks/useUndoRedo.js +83 -0
  36. package/dist/esm/index.js +16 -0
  37. package/dist/esm/storybook/index.js +1 -0
  38. package/dist/esm/storybook/mockData.js +73 -0
  39. package/dist/esm/types/columnTypes.js +1 -0
  40. package/dist/esm/types/dataGridTypes.js +1 -0
  41. package/dist/esm/types/index.js +1 -0
  42. package/dist/esm/utils/dataGridViewModel.js +220 -0
  43. package/dist/esm/utils/gridRowComparator.js +2 -0
  44. package/dist/esm/utils/index.js +5 -0
  45. package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
  46. package/dist/types/components/CellErrorBoundary.d.ts +25 -0
  47. package/dist/types/components/EmptyState.d.ts +26 -0
  48. package/dist/types/components/GridContextMenu.d.ts +18 -0
  49. package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
  50. package/dist/types/components/OGridLayout.d.ts +37 -0
  51. package/dist/types/components/SideBar.d.ts +30 -0
  52. package/dist/types/components/StatusBar.d.ts +24 -0
  53. package/dist/types/hooks/index.d.ts +48 -0
  54. package/dist/types/hooks/useActiveCell.d.ts +13 -0
  55. package/dist/types/hooks/useCellEditing.d.ts +16 -0
  56. package/dist/types/hooks/useCellSelection.d.ts +22 -0
  57. package/dist/types/hooks/useClipboard.d.ts +30 -0
  58. package/dist/types/hooks/useColumnChooserState.d.ts +27 -0
  59. package/dist/types/hooks/useColumnHeaderFilterState.d.ts +73 -0
  60. package/dist/types/hooks/useColumnResize.d.ts +23 -0
  61. package/dist/types/hooks/useContextMenu.d.ts +19 -0
  62. package/dist/types/hooks/useDataGridState.d.ts +137 -0
  63. package/dist/types/hooks/useDateFilterState.d.ts +19 -0
  64. package/dist/types/hooks/useDebounce.d.ts +9 -0
  65. package/dist/types/hooks/useFillHandle.d.ts +33 -0
  66. package/dist/types/hooks/useFilterOptions.d.ts +16 -0
  67. package/dist/types/hooks/useInlineCellEditorState.d.ts +24 -0
  68. package/dist/types/hooks/useKeyboardNavigation.d.ts +47 -0
  69. package/dist/types/hooks/useLatestRef.d.ts +6 -0
  70. package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
  71. package/dist/types/hooks/useOGrid.d.ts +52 -0
  72. package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
  73. package/dist/types/hooks/useRichSelectState.d.ts +22 -0
  74. package/dist/types/hooks/useRowSelection.d.ts +22 -0
  75. package/dist/types/hooks/useSideBarState.d.ts +20 -0
  76. package/dist/types/hooks/useTableLayout.d.ts +27 -0
  77. package/dist/types/hooks/useTextFilterState.d.ts +16 -0
  78. package/dist/types/hooks/useUndoRedo.d.ts +23 -0
  79. package/dist/types/index.d.ts +23 -0
  80. package/dist/types/storybook/index.d.ts +2 -0
  81. package/dist/types/storybook/mockData.d.ts +37 -0
  82. package/dist/types/types/columnTypes.d.ts +25 -0
  83. package/dist/types/types/dataGridTypes.d.ts +152 -0
  84. package/dist/types/types/index.d.ts +3 -0
  85. package/dist/types/utils/dataGridViewModel.d.ts +161 -0
  86. package/dist/types/utils/gridRowComparator.d.ts +2 -0
  87. package/dist/types/utils/index.d.ts +6 -0
  88. package/package.json +46 -0
@@ -0,0 +1,465 @@
1
+ import { useMemo, useCallback, useState, useEffect, useRef, useImperativeHandle, } from 'react';
2
+ import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, } from '../utils';
3
+ import { useFilterOptions } from './useFilterOptions';
4
+ import { useSideBarState } from './useSideBarState';
5
+ const DEFAULT_PAGE_SIZE = 25;
6
+ const EMPTY_LOADING_OPTIONS = {};
7
+ /**
8
+ * Top-level orchestration hook for OGrid: manages pagination, sorting, filtering, column visibility, and sidebar.
9
+ * @param props - All OGrid props (columns, data, callbacks, feature flags).
10
+ * @param ref - Forwarded ref for imperative API (refresh, export, applyColumnState).
11
+ * @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
12
+ */
13
+ export function useOGrid(props, ref) {
14
+ 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, freezeRows, freezeCols, 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, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
15
+ // Resolve column chooser placement
16
+ const columnChooserPlacement = columnChooserProp === false ? 'none'
17
+ : columnChooserProp === 'sidebar' ? 'sidebar'
18
+ : 'toolbar';
19
+ const columns = useMemo(() => flattenColumns(columnsProp), [columnsProp]);
20
+ const isServerSide = dataSource != null;
21
+ const isClientSide = !isServerSide;
22
+ const [internalData, setInternalData] = useState([]);
23
+ const [internalLoading, setInternalLoading] = useState(false);
24
+ const displayData = data ?? internalData;
25
+ 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,
32
+ });
33
+ const [internalFilters, setInternalFilters] = useState({});
34
+ const [internalVisibleColumns, setInternalVisibleColumns] = useState(() => {
35
+ const visible = columns
36
+ .filter((c) => c.defaultVisible !== false)
37
+ .map((c) => c.columnId);
38
+ return new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId));
39
+ });
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
+ 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
+ const setVisibleColumns = useCallback((cols) => {
71
+ if (controlledVisibleColumns === undefined)
72
+ setInternalVisibleColumns(cols);
73
+ onVisibleColumnsChange?.(cols);
74
+ }, [controlledVisibleColumns, onVisibleColumnsChange]);
75
+ const handleSort = useCallback((columnKey) => {
76
+ setSort({
77
+ field: columnKey,
78
+ direction: sort.field === columnKey && sort.direction === 'asc' ? 'desc' : 'asc',
79
+ });
80
+ }, [sort, setSort]);
81
+ /** Single filter change handler — wraps discriminated FilterValue into mergeFilter. */
82
+ const handleFilterChange = useCallback((key, value) => {
83
+ setFilters(mergeFilter(filters, key, value));
84
+ }, [filters, setFilters]);
85
+ const handleVisibilityChange = useCallback((columnKey, isVisible) => {
86
+ const next = new Set(visibleColumns);
87
+ if (isVisible)
88
+ next.add(columnKey);
89
+ else
90
+ next.delete(columnKey);
91
+ setVisibleColumns(next);
92
+ }, [visibleColumns, setVisibleColumns]);
93
+ const [internalSelectedRows, setInternalSelectedRows] = useState(new Set());
94
+ const effectiveSelectedRows = selectedRows ?? internalSelectedRows;
95
+ const handleSelectionChange = useCallback((event) => {
96
+ if (selectedRows === undefined) {
97
+ setInternalSelectedRows(new Set(event.selectedRowIds));
98
+ }
99
+ onSelectionChange?.(event);
100
+ }, [selectedRows, onSelectionChange]);
101
+ const multiSelectFilterFields = useMemo(() => getMultiSelectFilterFields(columns), [columns]);
102
+ const filterOptionsSource = useMemo(() => dataSource ?? { fetchFilterOptions: undefined }, [dataSource]);
103
+ const { filterOptions: serverFilterOptions, loadingOptions: loadingFilterOptions } = useFilterOptions(filterOptionsSource, multiSelectFilterFields);
104
+ const hasServerFilterOptions = dataSource?.fetchFilterOptions != null;
105
+ const clientFilterOptions = useMemo(() => {
106
+ if (hasServerFilterOptions)
107
+ return serverFilterOptions;
108
+ return deriveFilterOptionsFromData(displayData, columns);
109
+ }, [hasServerFilterOptions, displayData, columns, serverFilterOptions]);
110
+ // --- Client-side filtering & sorting ---
111
+ const clientItemsAndTotal = useMemo(() => {
112
+ if (!isClientSide)
113
+ return null;
114
+ const rows = processClientSideData(displayData, columns, filters, sort.field, sort.direction);
115
+ const total = rows.length;
116
+ const start = (page - 1) * pageSize;
117
+ const paged = rows.slice(start, start + pageSize);
118
+ return { items: paged, totalCount: total };
119
+ }, [
120
+ isClientSide,
121
+ displayData,
122
+ columns,
123
+ filters,
124
+ sort.field,
125
+ sort.direction,
126
+ page,
127
+ pageSize,
128
+ ]);
129
+ const [serverItems, setServerItems] = useState([]);
130
+ const [serverTotalCount, setServerTotalCount] = useState(0);
131
+ const [loading, setLoading] = useState(true);
132
+ const fetchIdRef = useRef(0);
133
+ // Ref counter to trigger server-side re-fetches
134
+ const refreshCounterRef = useRef(0);
135
+ const [refreshCounter, setRefreshCounter] = useState(0);
136
+ useEffect(() => {
137
+ if (!isServerSide || !dataSource) {
138
+ if (!isServerSide)
139
+ setLoading(false);
140
+ return;
141
+ }
142
+ const id = ++fetchIdRef.current;
143
+ setLoading(true);
144
+ dataSource
145
+ .fetchPage({
146
+ page,
147
+ pageSize,
148
+ sort: { field: sort.field, direction: sort.direction },
149
+ filters,
150
+ })
151
+ .then((res) => {
152
+ if (id !== fetchIdRef.current)
153
+ return;
154
+ setServerItems(res.items);
155
+ setServerTotalCount(res.totalCount);
156
+ })
157
+ .catch((err) => {
158
+ if (id !== fetchIdRef.current)
159
+ return;
160
+ onError?.(err);
161
+ setServerItems([]);
162
+ setServerTotalCount(0);
163
+ })
164
+ .finally(() => {
165
+ if (id === fetchIdRef.current)
166
+ setLoading(false);
167
+ });
168
+ }, [
169
+ isServerSide,
170
+ dataSource,
171
+ page,
172
+ pageSize,
173
+ sort.field,
174
+ sort.direction,
175
+ filters,
176
+ onError,
177
+ refreshCounter,
178
+ ]);
179
+ const displayItems = isClientSide && clientItemsAndTotal
180
+ ? clientItemsAndTotal.items
181
+ : serverItems;
182
+ const displayTotalCount = isClientSide && clientItemsAndTotal
183
+ ? clientItemsAndTotal.totalCount
184
+ : serverTotalCount;
185
+ // Fire onFirstDataRendered once when the grid first has data
186
+ const firstDataRenderedRef = useRef(false);
187
+ useEffect(() => {
188
+ if (!firstDataRenderedRef.current && displayItems.length > 0) {
189
+ firstDataRenderedRef.current = true;
190
+ onFirstDataRendered?.();
191
+ }
192
+ }, [displayItems.length, onFirstDataRendered]);
193
+ useImperativeHandle(ref, () => ({
194
+ setRowData: (d) => {
195
+ if (!isServerSide)
196
+ setInternalData(d);
197
+ },
198
+ setLoading: setInternalLoading,
199
+ getColumnState: () => ({
200
+ visibleColumns: Array.from(visibleColumns),
201
+ sort,
202
+ columnOrder: columnOrder ?? undefined,
203
+ columnWidths: Object.keys(columnWidthOverrides).length > 0 ? columnWidthOverrides : undefined,
204
+ filters: Object.keys(filters).length > 0 ? filters : undefined,
205
+ pinnedColumns: Object.keys(pinnedOverrides).length > 0 ? pinnedOverrides : undefined,
206
+ }),
207
+ applyColumnState: (state) => {
208
+ if (state.visibleColumns) {
209
+ setVisibleColumns(new Set(state.visibleColumns));
210
+ }
211
+ if (state.sort) {
212
+ setSort(state.sort);
213
+ }
214
+ if (state.columnOrder && onColumnOrderChange) {
215
+ onColumnOrderChange(state.columnOrder);
216
+ }
217
+ if (state.columnWidths) {
218
+ setColumnWidthOverrides(state.columnWidths);
219
+ }
220
+ if (state.filters) {
221
+ setFilters(state.filters);
222
+ }
223
+ if (state.pinnedColumns) {
224
+ setPinnedOverrides(state.pinnedColumns);
225
+ }
226
+ },
227
+ setFilterModel: setFilters,
228
+ getSelectedRows: () => Array.from(effectiveSelectedRows),
229
+ setSelectedRows: (rowIds) => {
230
+ if (selectedRows === undefined)
231
+ setInternalSelectedRows(new Set(rowIds));
232
+ },
233
+ selectAll: () => {
234
+ const allIds = new Set(displayItems.map((item) => getRowId(item)));
235
+ if (selectedRows === undefined)
236
+ setInternalSelectedRows(allIds);
237
+ onSelectionChange?.({
238
+ selectedRowIds: Array.from(allIds),
239
+ selectedItems: displayItems,
240
+ });
241
+ },
242
+ deselectAll: () => {
243
+ if (selectedRows === undefined)
244
+ setInternalSelectedRows(new Set());
245
+ onSelectionChange?.({
246
+ selectedRowIds: [],
247
+ selectedItems: [],
248
+ });
249
+ },
250
+ clearFilters: () => setFilters({}),
251
+ clearSort: () => setSort({ field: defaultSortField, direction: defaultSortDirection }),
252
+ resetGridState: (options) => {
253
+ setFilters({});
254
+ setSort({ field: defaultSortField, direction: defaultSortDirection });
255
+ if (!options?.keepSelection) {
256
+ if (selectedRows === undefined)
257
+ setInternalSelectedRows(new Set());
258
+ onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
259
+ }
260
+ },
261
+ getDisplayedRows: () => displayItems,
262
+ refreshData: () => {
263
+ if (isServerSide) {
264
+ refreshCounterRef.current += 1;
265
+ setRefreshCounter(refreshCounterRef.current);
266
+ }
267
+ },
268
+ }), [
269
+ visibleColumns,
270
+ sort,
271
+ columnOrder,
272
+ columnWidthOverrides,
273
+ pinnedOverrides,
274
+ filters,
275
+ setFilters,
276
+ setSort,
277
+ setVisibleColumns,
278
+ onColumnOrderChange,
279
+ isServerSide,
280
+ effectiveSelectedRows,
281
+ selectedRows,
282
+ displayItems,
283
+ getRowId,
284
+ onSelectionChange,
285
+ defaultSortField,
286
+ defaultSortDirection,
287
+ ]);
288
+ // With discriminated union, any defined value is active (mergeFilter already strips empties)
289
+ const hasActiveFilters = useMemo(() => {
290
+ return Object.values(filters).some((v) => v !== undefined);
291
+ }, [filters]);
292
+ const columnChooserColumns = useMemo(() => columns.map((c) => ({
293
+ columnId: c.columnId,
294
+ name: c.name,
295
+ required: c.required === true,
296
+ })), [columns]);
297
+ const statusBarConfig = useMemo(() => {
298
+ if (!statusBar)
299
+ return undefined;
300
+ if (typeof statusBar === 'object')
301
+ return statusBar;
302
+ const totalData = isClientSide ? (data?.length ?? 0) : serverTotalCount;
303
+ const filteredData = displayTotalCount;
304
+ return {
305
+ totalCount: totalData,
306
+ filteredCount: hasActiveFilters ? filteredData : undefined,
307
+ selectedCount: effectiveSelectedRows.size,
308
+ suppressRowCount: true, // OGrid always has pagination which shows the total
309
+ };
310
+ }, [
311
+ statusBar,
312
+ isClientSide,
313
+ data,
314
+ serverTotalCount,
315
+ displayTotalCount,
316
+ hasActiveFilters,
317
+ effectiveSelectedRows.size,
318
+ ]);
319
+ const handleColumnResized = useCallback((columnId, width) => {
320
+ setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
321
+ onColumnResized?.(columnId, width);
322
+ }, [onColumnResized]);
323
+ const handleColumnPinned = useCallback((columnId, pinned) => {
324
+ setPinnedOverrides((prev) => {
325
+ if (pinned === null) {
326
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
327
+ const { [columnId]: _, ...rest } = prev;
328
+ return rest;
329
+ }
330
+ return { ...prev, [columnId]: pinned };
331
+ });
332
+ onColumnPinned?.(columnId, pinned);
333
+ }, [onColumnPinned]);
334
+ // --- Side bar ---
335
+ const sideBarState = useSideBarState({ config: sideBar });
336
+ const filterableColumns = useMemo(() => columns
337
+ .filter((c) => c.filterable && c.filterable.type)
338
+ .map((c) => ({
339
+ columnId: c.columnId,
340
+ name: c.name,
341
+ filterField: c.filterable.filterField ?? c.columnId,
342
+ filterType: c.filterable.type,
343
+ })), [columns]);
344
+ const sideBarProps = useMemo(() => {
345
+ if (!sideBarState.isEnabled)
346
+ return null;
347
+ return {
348
+ activePanel: sideBarState.activePanel,
349
+ onPanelChange: sideBarState.setActivePanel,
350
+ panels: sideBarState.panels,
351
+ position: sideBarState.position,
352
+ columns: columnChooserColumns,
353
+ visibleColumns,
354
+ onVisibilityChange: handleVisibilityChange,
355
+ onSetVisibleColumns: setVisibleColumns,
356
+ filterableColumns,
357
+ filters,
358
+ onFilterChange: handleFilterChange,
359
+ filterOptions: clientFilterOptions,
360
+ };
361
+ }, [
362
+ sideBarState.isEnabled,
363
+ sideBarState.activePanel,
364
+ sideBarState.setActivePanel,
365
+ sideBarState.panels,
366
+ sideBarState.position,
367
+ columnChooserColumns,
368
+ visibleColumns,
369
+ handleVisibilityChange,
370
+ setVisibleColumns,
371
+ filterableColumns,
372
+ filters,
373
+ handleFilterChange,
374
+ clientFilterOptions,
375
+ ]);
376
+ const clearAllFilters = useCallback(() => setFilters({}), [setFilters]);
377
+ const isLoadingResolved = (isServerSide && loading) || displayLoading;
378
+ const dataGridProps = useMemo(() => ({
379
+ items: displayItems,
380
+ columns: columnsProp,
381
+ getRowId,
382
+ sortBy: sort.field,
383
+ sortDirection: sort.direction,
384
+ onColumnSort: handleSort,
385
+ visibleColumns,
386
+ columnOrder,
387
+ onColumnOrderChange,
388
+ onColumnResized: handleColumnResized,
389
+ onColumnPinned: handleColumnPinned,
390
+ pinnedColumns: pinnedOverrides,
391
+ initialColumnWidths: columnWidthOverrides,
392
+ freezeRows,
393
+ freezeCols,
394
+ editable,
395
+ cellSelection,
396
+ onCellValueChanged,
397
+ onUndo,
398
+ onRedo,
399
+ canUndo,
400
+ canRedo,
401
+ rowSelection,
402
+ selectedRows: effectiveSelectedRows,
403
+ onSelectionChange: handleSelectionChange,
404
+ statusBar: statusBarConfig,
405
+ isLoading: isLoadingResolved,
406
+ filters,
407
+ onFilterChange: handleFilterChange,
408
+ filterOptions: clientFilterOptions,
409
+ loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : EMPTY_LOADING_OPTIONS,
410
+ peopleSearch: dataSource?.searchPeople,
411
+ getUserByEmail: dataSource?.getUserByEmail,
412
+ layoutMode,
413
+ suppressHorizontalScroll,
414
+ 'aria-label': ariaLabel,
415
+ 'aria-labelledby': ariaLabelledBy,
416
+ emptyState: {
417
+ hasActiveFilters,
418
+ onClearAll: clearAllFilters,
419
+ message: emptyState?.message,
420
+ render: emptyState?.render,
421
+ },
422
+ }), [
423
+ displayItems, columnsProp, getRowId, sort.field, sort.direction, handleSort,
424
+ visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
425
+ handleColumnPinned, pinnedOverrides, columnWidthOverrides, freezeRows, freezeCols,
426
+ editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo,
427
+ rowSelection, effectiveSelectedRows, handleSelectionChange, statusBarConfig,
428
+ isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
429
+ loadingFilterOptions, layoutMode, suppressHorizontalScroll, ariaLabel, ariaLabelledBy,
430
+ hasActiveFilters, clearAllFilters, emptyState,
431
+ ]);
432
+ const pagination = useMemo(() => ({
433
+ page,
434
+ pageSize,
435
+ displayTotalCount,
436
+ setPage,
437
+ setPageSize,
438
+ pageSizeOptions,
439
+ entityLabelPlural,
440
+ }), [page, pageSize, displayTotalCount, setPage, setPageSize, pageSizeOptions, entityLabelPlural]);
441
+ const columnChooser = useMemo(() => ({
442
+ columns: columnChooserColumns,
443
+ visibleColumns,
444
+ onVisibilityChange: handleVisibilityChange,
445
+ placement: columnChooserPlacement,
446
+ }), [columnChooserColumns, visibleColumns, handleVisibilityChange, columnChooserPlacement]);
447
+ const layout = useMemo(() => ({
448
+ toolbar,
449
+ toolbarBelow,
450
+ className,
451
+ emptyState,
452
+ sideBarProps,
453
+ }), [toolbar, toolbarBelow, className, emptyState, sideBarProps]);
454
+ const filtersResult = useMemo(() => ({
455
+ hasActiveFilters,
456
+ setFilters,
457
+ }), [hasActiveFilters, setFilters]);
458
+ return {
459
+ dataGridProps,
460
+ pagination,
461
+ columnChooser,
462
+ layout,
463
+ filters: filtersResult,
464
+ };
465
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * People filter state sub-hook for column header filters.
3
+ * Manages people search text, suggestions, loading state, input ref, and user select/clear handlers.
4
+ * Includes debounced people search effect.
5
+ */
6
+ import { useState, useCallback, useEffect, useRef } from 'react';
7
+ const PEOPLE_SEARCH_DEBOUNCE_MS = 300;
8
+ export function usePeopleFilterState(params) {
9
+ const { onUserChange, peopleSearch, isFilterOpen, filterType } = params;
10
+ const peopleInputRef = useRef(null);
11
+ const peopleSearchTimeoutRef = useRef(undefined);
12
+ const [peopleSuggestions, setPeopleSuggestions] = useState([]);
13
+ const [isPeopleLoading, setIsPeopleLoading] = useState(false);
14
+ const [peopleSearchText, setPeopleSearchText] = useState('');
15
+ // Sync temp state when popover opens
16
+ useEffect(() => {
17
+ if (isFilterOpen) {
18
+ setPeopleSearchText('');
19
+ setPeopleSuggestions([]);
20
+ if (filterType === 'people') {
21
+ setTimeout(() => peopleInputRef.current?.focus(), 50);
22
+ }
23
+ }
24
+ }, [isFilterOpen, filterType]);
25
+ // People search with debounce
26
+ useEffect(() => {
27
+ if (!peopleSearch || !isFilterOpen || filterType !== 'people')
28
+ return;
29
+ if (peopleSearchTimeoutRef.current)
30
+ window.clearTimeout(peopleSearchTimeoutRef.current);
31
+ if (!peopleSearchText.trim()) {
32
+ setPeopleSuggestions([]);
33
+ return;
34
+ }
35
+ setIsPeopleLoading(true);
36
+ peopleSearchTimeoutRef.current = window.setTimeout(async () => {
37
+ try {
38
+ const results = await peopleSearch(peopleSearchText);
39
+ setPeopleSuggestions(results.slice(0, 10));
40
+ }
41
+ catch {
42
+ setPeopleSuggestions([]);
43
+ }
44
+ finally {
45
+ setIsPeopleLoading(false);
46
+ }
47
+ }, PEOPLE_SEARCH_DEBOUNCE_MS);
48
+ return () => {
49
+ if (peopleSearchTimeoutRef.current)
50
+ window.clearTimeout(peopleSearchTimeoutRef.current);
51
+ };
52
+ }, [peopleSearchText, peopleSearch, isFilterOpen, filterType]);
53
+ const handleUserSelect = useCallback((user) => {
54
+ onUserChange?.(user);
55
+ }, [onUserChange]);
56
+ const handleClearUser = useCallback(() => {
57
+ onUserChange?.(undefined);
58
+ }, [onUserChange]);
59
+ return {
60
+ peopleSuggestions,
61
+ isPeopleLoading,
62
+ peopleSearchText,
63
+ setPeopleSearchText,
64
+ peopleInputRef,
65
+ handleUserSelect,
66
+ handleClearUser,
67
+ };
68
+ }
@@ -0,0 +1,58 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ /**
3
+ * Manages searchable rich select editor state with keyboard navigation (arrow keys, enter, escape).
4
+ * @param params - Values, format function, initial value, and commit/cancel callbacks.
5
+ * @returns Search text, filtered values, highlighted index, keyboard handler, and select function.
6
+ */
7
+ export function useRichSelectState(params) {
8
+ const { values, formatValue, onCommit, onCancel } = params;
9
+ const [searchText, setSearchText] = useState('');
10
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
11
+ const getDisplayText = useCallback((value) => {
12
+ if (formatValue)
13
+ return formatValue(value);
14
+ return value != null ? String(value) : '';
15
+ }, [formatValue]);
16
+ const filteredValues = useMemo(() => {
17
+ if (!searchText.trim())
18
+ return values;
19
+ const lower = searchText.toLowerCase();
20
+ return values.filter((v) => getDisplayText(v).toLowerCase().includes(lower));
21
+ }, [values, searchText, getDisplayText]);
22
+ const selectValue = useCallback((value) => {
23
+ onCommit(value);
24
+ }, [onCommit]);
25
+ const handleKeyDown = useCallback((e) => {
26
+ switch (e.key) {
27
+ case 'ArrowDown':
28
+ e.preventDefault();
29
+ setHighlightedIndex((prev) => Math.min(prev + 1, filteredValues.length - 1));
30
+ break;
31
+ case 'ArrowUp':
32
+ e.preventDefault();
33
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
34
+ break;
35
+ case 'Enter':
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ if (filteredValues.length > 0 && highlightedIndex < filteredValues.length) {
39
+ selectValue(filteredValues[highlightedIndex]);
40
+ }
41
+ break;
42
+ case 'Escape':
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+ onCancel();
46
+ break;
47
+ }
48
+ }, [filteredValues, highlightedIndex, selectValue, onCancel]);
49
+ return {
50
+ searchText,
51
+ setSearchText,
52
+ filteredValues,
53
+ highlightedIndex,
54
+ handleKeyDown,
55
+ selectValue,
56
+ getDisplayText,
57
+ };
58
+ }
@@ -0,0 +1,80 @@
1
+ import { useState, useCallback, useRef, useMemo } from 'react';
2
+ import { useLatestRef } from './useLatestRef';
3
+ /**
4
+ * Manages row selection state for single or multiple selection modes with shift-click range support.
5
+ * @param params - Items, getRowId, selection mode, controlled state, and selection change callback.
6
+ * @returns Selected row IDs, update function, checkbox handlers, and selection state booleans.
7
+ */
8
+ export function useRowSelection(params) {
9
+ const { items, getRowId, rowSelection, controlledSelectedRows, onSelectionChange, } = params;
10
+ const [internalSelectedRows, setInternalSelectedRows] = useState(new Set());
11
+ const lastClickedRowRef = useRef(-1);
12
+ // Defensive: convert to Set if caller passes an array (e.g. from JSON state)
13
+ const selectedRowIds = useMemo(() => controlledSelectedRows != null
14
+ ? controlledSelectedRows instanceof Set
15
+ ? controlledSelectedRows
16
+ : new Set(controlledSelectedRows)
17
+ : internalSelectedRows, [controlledSelectedRows, internalSelectedRows]);
18
+ const updateSelection = useCallback((newSelectedIds) => {
19
+ if (controlledSelectedRows === undefined) {
20
+ setInternalSelectedRows(newSelectedIds);
21
+ }
22
+ onSelectionChange?.({
23
+ selectedRowIds: Array.from(newSelectedIds),
24
+ selectedItems: items.filter((item) => newSelectedIds.has(getRowId(item))),
25
+ });
26
+ }, [controlledSelectedRows, onSelectionChange, items, getRowId]);
27
+ // Read selectedRowIds via ref to avoid recreating this callback on every selection change
28
+ const selectedRowIdsRef = useLatestRef(selectedRowIds);
29
+ const itemsRef = useLatestRef(items);
30
+ const handleRowCheckboxChange = useCallback((rowId, checked, rowIndex, shiftKey) => {
31
+ if (rowSelection === 'single') {
32
+ updateSelection(checked ? new Set([rowId]) : new Set());
33
+ lastClickedRowRef.current = rowIndex;
34
+ return;
35
+ }
36
+ const next = new Set(selectedRowIdsRef.current);
37
+ const currentItems = itemsRef.current;
38
+ if (shiftKey && lastClickedRowRef.current >= 0 && lastClickedRowRef.current !== rowIndex) {
39
+ const start = Math.min(lastClickedRowRef.current, rowIndex);
40
+ const end = Math.max(lastClickedRowRef.current, rowIndex);
41
+ for (let i = start; i <= end; i++) {
42
+ if (i < currentItems.length) {
43
+ const id = getRowId(currentItems[i]);
44
+ if (checked)
45
+ next.add(id);
46
+ else
47
+ next.delete(id);
48
+ }
49
+ }
50
+ }
51
+ else {
52
+ if (checked)
53
+ next.add(rowId);
54
+ else
55
+ next.delete(rowId);
56
+ }
57
+ lastClickedRowRef.current = rowIndex;
58
+ updateSelection(next);
59
+ },
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- itemsRef, selectedRowIdsRef are stable refs
61
+ [rowSelection, getRowId, updateSelection]);
62
+ const handleSelectAll = useCallback((checked) => {
63
+ if (checked) {
64
+ updateSelection(new Set(items.map((item) => getRowId(item))));
65
+ }
66
+ else {
67
+ updateSelection(new Set());
68
+ }
69
+ }, [items, getRowId, updateSelection]);
70
+ const allSelected = useMemo(() => items.length > 0 && items.every((item) => selectedRowIds.has(getRowId(item))), [items, selectedRowIds, getRowId]);
71
+ const someSelected = useMemo(() => !allSelected && items.some((item) => selectedRowIds.has(getRowId(item))), [allSelected, items, selectedRowIds, getRowId]);
72
+ return {
73
+ selectedRowIds,
74
+ updateSelection,
75
+ handleRowCheckboxChange,
76
+ handleSelectAll,
77
+ allSelected,
78
+ someSelected,
79
+ };
80
+ }