@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.
- package/dist/esm/components/ColumnHeaderFilterContent.js +0 -2
- package/dist/esm/components/MarchingAntsOverlay.js +2 -3
- package/dist/esm/components/SideBar.js +8 -7
- package/dist/esm/components/createOGrid.js +1 -4
- package/dist/esm/hooks/index.js +10 -0
- package/dist/esm/hooks/useActiveCell.js +2 -4
- package/dist/esm/hooks/useCellSelection.js +85 -52
- package/dist/esm/hooks/useClipboard.js +15 -54
- package/dist/esm/hooks/useColumnChooserState.js +25 -13
- package/dist/esm/hooks/useColumnHeaderFilterState.js +22 -11
- package/dist/esm/hooks/useColumnHeaderMenuState.js +1 -1
- package/dist/esm/hooks/useColumnMeta.js +61 -0
- package/dist/esm/hooks/useColumnPinning.js +11 -12
- package/dist/esm/hooks/useColumnReorder.js +8 -1
- package/dist/esm/hooks/useColumnResize.js +6 -2
- package/dist/esm/hooks/useDataGridContextMenu.js +24 -0
- package/dist/esm/hooks/useDataGridEditing.js +56 -0
- package/dist/esm/hooks/useDataGridInteraction.js +109 -0
- package/dist/esm/hooks/useDataGridLayout.js +172 -0
- package/dist/esm/hooks/useDataGridState.js +83 -318
- package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -4
- package/dist/esm/hooks/useFillHandle.js +60 -55
- package/dist/esm/hooks/useFilterOptions.js +2 -4
- package/dist/esm/hooks/useInlineCellEditorState.js +7 -13
- package/dist/esm/hooks/useKeyboardNavigation.js +19 -132
- package/dist/esm/hooks/useMultiSelectFilterState.js +1 -1
- package/dist/esm/hooks/useOGrid.js +159 -301
- package/dist/esm/hooks/useOGridDataFetching.js +74 -0
- package/dist/esm/hooks/useOGridFilters.js +59 -0
- package/dist/esm/hooks/useOGridPagination.js +24 -0
- package/dist/esm/hooks/useOGridSorting.js +24 -0
- package/dist/esm/hooks/usePaginationControls.js +2 -5
- package/dist/esm/hooks/usePeopleFilterState.js +6 -1
- package/dist/esm/hooks/useRichSelectState.js +7 -5
- package/dist/esm/hooks/useRowSelection.js +6 -26
- package/dist/esm/hooks/useSelectState.js +2 -5
- package/dist/esm/hooks/useShallowEqualMemo.js +14 -0
- package/dist/esm/hooks/useTableLayout.js +3 -11
- package/dist/esm/hooks/useUndoRedo.js +16 -10
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/types/components/ColumnChooserProps.d.ts +2 -0
- package/dist/types/components/ColumnHeaderFilterContent.d.ts +0 -2
- package/dist/types/hooks/index.d.ts +19 -0
- package/dist/types/hooks/useClipboard.d.ts +0 -1
- package/dist/types/hooks/useColumnChooserState.d.ts +2 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -2
- package/dist/types/hooks/useColumnHeaderMenuState.d.ts +0 -2
- package/dist/types/hooks/useColumnMeta.d.ts +34 -0
- package/dist/types/hooks/useDataGridContextMenu.d.ts +20 -0
- package/dist/types/hooks/useDataGridEditing.d.ts +39 -0
- package/dist/types/hooks/useDataGridInteraction.d.ts +95 -0
- package/dist/types/hooks/useDataGridLayout.d.ts +45 -0
- package/dist/types/hooks/useDataGridState.d.ts +7 -1
- package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -2
- package/dist/types/hooks/useOGrid.d.ts +4 -2
- package/dist/types/hooks/useOGridDataFetching.d.ts +29 -0
- package/dist/types/hooks/useOGridFilters.d.ts +24 -0
- package/dist/types/hooks/useOGridPagination.d.ts +18 -0
- package/dist/types/hooks/useOGridSorting.d.ts +23 -0
- package/dist/types/hooks/usePaginationControls.d.ts +1 -1
- package/dist/types/hooks/useRichSelectState.d.ts +2 -0
- package/dist/types/hooks/useShallowEqualMemo.d.ts +7 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/utils/index.d.ts +2 -2
- package/package.json +12 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { processClientSideData } from '../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Manages data fetching (server-side) and client-side filtering/sorting/pagination.
|
|
5
|
+
* Fires onFirstDataRendered once when items first appear.
|
|
6
|
+
*/
|
|
7
|
+
export function useOGridDataFetching(params) {
|
|
8
|
+
const { isServerSide, dataSource, displayData, columns, stableFilters, filters, sort, page, pageSize, onError, onFirstDataRendered, } = params;
|
|
9
|
+
const isClientSide = !isServerSide;
|
|
10
|
+
// --- Client-side filtering & sorting ---
|
|
11
|
+
const clientItemsAndTotal = useMemo(() => {
|
|
12
|
+
if (!isClientSide)
|
|
13
|
+
return null;
|
|
14
|
+
const rows = processClientSideData(displayData, columns, stableFilters, sort.field, sort.direction);
|
|
15
|
+
const total = rows.length;
|
|
16
|
+
const start = (page - 1) * pageSize;
|
|
17
|
+
const paged = rows.slice(start, start + pageSize);
|
|
18
|
+
return { items: paged, totalCount: total };
|
|
19
|
+
}, [isClientSide, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
|
|
20
|
+
// --- Server-side data fetching ---
|
|
21
|
+
const [serverItems, setServerItems] = useState([]);
|
|
22
|
+
const [serverTotalCount, setServerTotalCount] = useState(0);
|
|
23
|
+
const [serverLoading, setServerLoading] = useState(true);
|
|
24
|
+
const fetchIdRef = useRef(0);
|
|
25
|
+
const [refreshCounter, setRefreshCounter] = useState(0);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isServerSide || !dataSource) {
|
|
28
|
+
if (!isServerSide)
|
|
29
|
+
setServerLoading(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const id = ++fetchIdRef.current;
|
|
33
|
+
setServerLoading(true);
|
|
34
|
+
dataSource
|
|
35
|
+
.fetchPage({
|
|
36
|
+
page, pageSize,
|
|
37
|
+
sort: { field: sort.field, direction: sort.direction },
|
|
38
|
+
filters,
|
|
39
|
+
})
|
|
40
|
+
.then((res) => {
|
|
41
|
+
if (id !== fetchIdRef.current)
|
|
42
|
+
return;
|
|
43
|
+
setServerItems(res.items);
|
|
44
|
+
setServerTotalCount(res.totalCount);
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => {
|
|
47
|
+
if (id !== fetchIdRef.current)
|
|
48
|
+
return;
|
|
49
|
+
onError?.(err);
|
|
50
|
+
setServerItems([]);
|
|
51
|
+
setServerTotalCount(0);
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
if (id === fetchIdRef.current)
|
|
55
|
+
setServerLoading(false);
|
|
56
|
+
});
|
|
57
|
+
}, [isServerSide, dataSource, page, pageSize, sort.field, sort.direction, filters, onError, refreshCounter]);
|
|
58
|
+
const displayItems = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.items : serverItems;
|
|
59
|
+
const displayTotalCount = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.totalCount : serverTotalCount;
|
|
60
|
+
// Fire onFirstDataRendered once when the grid first has data
|
|
61
|
+
const firstDataRenderedRef = useRef(false);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!firstDataRenderedRef.current && displayItems.length > 0) {
|
|
64
|
+
firstDataRenderedRef.current = true;
|
|
65
|
+
onFirstDataRendered?.();
|
|
66
|
+
}
|
|
67
|
+
}, [displayItems.length, onFirstDataRendered]);
|
|
68
|
+
return {
|
|
69
|
+
displayItems,
|
|
70
|
+
displayTotalCount,
|
|
71
|
+
serverLoading,
|
|
72
|
+
refreshData: () => setRefreshCounter((prev) => prev + 1),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from '../utils';
|
|
3
|
+
import { useFilterOptions } from './useFilterOptions';
|
|
4
|
+
import { useShallowEqualMemo } from './useShallowEqualMemo';
|
|
5
|
+
/** Deep-equal check for filter objects (shallow key+value comparison). */
|
|
6
|
+
function filtersEqual(a, b) {
|
|
7
|
+
const aKeys = Object.keys(a);
|
|
8
|
+
const bKeys = Object.keys(b);
|
|
9
|
+
if (aKeys.length !== bKeys.length)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < bKeys.length; i++) {
|
|
12
|
+
if (a[bKeys[i]] !== b[bKeys[i]])
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const EMPTY_LOADING_OPTIONS = {};
|
|
18
|
+
const EMPTY_DATA_SOURCE = { fetchFilterOptions: undefined };
|
|
19
|
+
/**
|
|
20
|
+
* Manages filter state, filter options (client + server), and stabilized filter reference.
|
|
21
|
+
* Resets to page 1 on filter change.
|
|
22
|
+
*/
|
|
23
|
+
export function useOGridFilters(params) {
|
|
24
|
+
const { controlledFilters, onFiltersChange, setPage, columns, displayData, dataSource } = params;
|
|
25
|
+
const [internalFilters, setInternalFilters] = useState({});
|
|
26
|
+
const filters = controlledFilters ?? internalFilters;
|
|
27
|
+
const setFilters = useCallback((f) => {
|
|
28
|
+
if (controlledFilters === undefined)
|
|
29
|
+
setInternalFilters(f);
|
|
30
|
+
onFiltersChange?.(f);
|
|
31
|
+
setPage(1);
|
|
32
|
+
}, [controlledFilters, onFiltersChange, setPage]);
|
|
33
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
34
|
+
setFilters(mergeFilter(filters, key, value));
|
|
35
|
+
}, [filters, setFilters]);
|
|
36
|
+
// Stabilize filters via shallow comparison so processClientSideData useMemo
|
|
37
|
+
// doesn't re-run when the filter object reference changes but values are identical.
|
|
38
|
+
const stableFilters = useShallowEqualMemo(filters, (a, b) => filtersEqual(a, b));
|
|
39
|
+
const hasActiveFilters = useMemo(() => Object.values(filters).some((v) => v !== undefined), [filters]);
|
|
40
|
+
// --- Filter options (server or client-derived) ---
|
|
41
|
+
const multiSelectFilterFields = useMemo(() => getMultiSelectFilterFields(columns), [columns]);
|
|
42
|
+
const filterOptionsSource = dataSource ?? EMPTY_DATA_SOURCE;
|
|
43
|
+
const { filterOptions: serverFilterOptions, loadingOptions: loadingFilterOptions } = useFilterOptions(filterOptionsSource, multiSelectFilterFields);
|
|
44
|
+
const hasServerFilterOptions = dataSource?.fetchFilterOptions != null;
|
|
45
|
+
const clientFilterOptions = useMemo(() => {
|
|
46
|
+
if (hasServerFilterOptions)
|
|
47
|
+
return serverFilterOptions;
|
|
48
|
+
return deriveFilterOptionsFromData(displayData, columns);
|
|
49
|
+
}, [hasServerFilterOptions, displayData, columns, serverFilterOptions]);
|
|
50
|
+
return {
|
|
51
|
+
filters,
|
|
52
|
+
setFilters,
|
|
53
|
+
handleFilterChange,
|
|
54
|
+
stableFilters,
|
|
55
|
+
hasActiveFilters,
|
|
56
|
+
clientFilterOptions,
|
|
57
|
+
loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : EMPTY_LOADING_OPTIONS,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Manages pagination state with controlled/uncontrolled dual-mode support.
|
|
4
|
+
* Resets to page 1 when page size changes.
|
|
5
|
+
*/
|
|
6
|
+
export function useOGridPagination(params) {
|
|
7
|
+
const { controlledPage, controlledPageSize, defaultPageSize, onPageChange, onPageSizeChange } = params;
|
|
8
|
+
const [internalPage, setInternalPage] = useState(1);
|
|
9
|
+
const [internalPageSize, setInternalPageSize] = useState(defaultPageSize);
|
|
10
|
+
const page = controlledPage ?? internalPage;
|
|
11
|
+
const pageSize = controlledPageSize ?? internalPageSize;
|
|
12
|
+
const setPage = useCallback((p) => {
|
|
13
|
+
if (controlledPage === undefined)
|
|
14
|
+
setInternalPage(p);
|
|
15
|
+
onPageChange?.(p);
|
|
16
|
+
}, [controlledPage, onPageChange]);
|
|
17
|
+
const setPageSize = useCallback((size) => {
|
|
18
|
+
if (controlledPageSize === undefined)
|
|
19
|
+
setInternalPageSize(size);
|
|
20
|
+
onPageSizeChange?.(size);
|
|
21
|
+
setPage(1);
|
|
22
|
+
}, [controlledPageSize, onPageSizeChange, setPage]);
|
|
23
|
+
return { page, pageSize, setPage, setPageSize };
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { computeNextSortState } from '../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Manages sort state with controlled/uncontrolled dual-mode support.
|
|
5
|
+
* Resets to page 1 on sort change.
|
|
6
|
+
*/
|
|
7
|
+
export function useOGridSorting(params) {
|
|
8
|
+
const { controlledSort, defaultSortField, defaultSortDirection, onSortChange, setPage } = params;
|
|
9
|
+
const [internalSort, setInternalSort] = useState({
|
|
10
|
+
field: defaultSortField,
|
|
11
|
+
direction: defaultSortDirection,
|
|
12
|
+
});
|
|
13
|
+
const sort = controlledSort ?? internalSort;
|
|
14
|
+
const setSort = useCallback((s) => {
|
|
15
|
+
if (controlledSort === undefined)
|
|
16
|
+
setInternalSort(s);
|
|
17
|
+
onSortChange?.(s);
|
|
18
|
+
setPage(1);
|
|
19
|
+
}, [controlledSort, onSortChange, setPage]);
|
|
20
|
+
const handleSort = useCallback((columnKey, direction) => {
|
|
21
|
+
setSort(computeNextSortState(sort, columnKey, direction));
|
|
22
|
+
}, [sort, setSort]);
|
|
23
|
+
return { sort, setSort, handleSort, defaultSortField, defaultSortDirection };
|
|
24
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
2
|
import { getPaginationViewModel } from '../utils';
|
|
3
3
|
/**
|
|
4
4
|
* Shared pagination controls logic for React UI packages.
|
|
@@ -8,12 +8,9 @@ export function usePaginationControls(props) {
|
|
|
8
8
|
const { currentPage, pageSize, totalCount, onPageSizeChange, pageSizeOptions, entityLabelPlural } = props;
|
|
9
9
|
const labelPlural = entityLabelPlural ?? 'items';
|
|
10
10
|
const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
|
|
11
|
-
const handlePageSizeChange = useCallback((value) => {
|
|
12
|
-
onPageSizeChange(value);
|
|
13
|
-
}, [onPageSizeChange]);
|
|
14
11
|
return {
|
|
15
12
|
labelPlural,
|
|
16
13
|
vm,
|
|
17
|
-
handlePageSizeChange,
|
|
14
|
+
handlePageSizeChange: onPageSizeChange,
|
|
18
15
|
};
|
|
19
16
|
}
|
|
@@ -8,6 +8,7 @@ import { PEOPLE_SEARCH_DEBOUNCE_MS } from '@alaarab/ogrid-core';
|
|
|
8
8
|
export function usePeopleFilterState(params) {
|
|
9
9
|
const { onUserChange, peopleSearch, isFilterOpen, filterType } = params;
|
|
10
10
|
const peopleInputRef = useRef(null);
|
|
11
|
+
const focusTimeoutRef = useRef(undefined);
|
|
11
12
|
const peopleSearchTimeoutRef = useRef(undefined);
|
|
12
13
|
const [peopleSuggestions, setPeopleSuggestions] = useState([]);
|
|
13
14
|
const [isPeopleLoading, setIsPeopleLoading] = useState(false);
|
|
@@ -18,9 +19,13 @@ export function usePeopleFilterState(params) {
|
|
|
18
19
|
setPeopleSearchText('');
|
|
19
20
|
setPeopleSuggestions([]);
|
|
20
21
|
if (filterType === 'people') {
|
|
21
|
-
setTimeout(() => peopleInputRef.current?.focus(), 50);
|
|
22
|
+
focusTimeoutRef.current = window.setTimeout(() => peopleInputRef.current?.focus(), 50);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
return () => {
|
|
26
|
+
if (focusTimeoutRef.current)
|
|
27
|
+
window.clearTimeout(focusTimeoutRef.current);
|
|
28
|
+
};
|
|
24
29
|
}, [isFilterOpen, filterType]);
|
|
25
30
|
// People search with debounce
|
|
26
31
|
useEffect(() => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
/** Shared display text formatter for select and rich-select editors. */
|
|
3
|
+
export function getSelectDisplayText(value, formatValue) {
|
|
4
|
+
if (formatValue)
|
|
5
|
+
return formatValue(value);
|
|
6
|
+
return value != null ? String(value) : '';
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* Manages searchable rich select editor state with keyboard navigation (arrow keys, enter, escape).
|
|
4
10
|
* @param params - Values, format function, initial value, and commit/cancel callbacks.
|
|
@@ -8,11 +14,7 @@ export function useRichSelectState(params) {
|
|
|
8
14
|
const { values, formatValue, onCommit, onCancel } = params;
|
|
9
15
|
const [searchText, setSearchText] = useState('');
|
|
10
16
|
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]);
|
|
17
|
+
const getDisplayText = useCallback((value) => getSelectDisplayText(value, formatValue), [formatValue]);
|
|
16
18
|
const filteredValues = useMemo(() => {
|
|
17
19
|
if (!searchText.trim())
|
|
18
20
|
return values;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useMemo } from 'react';
|
|
2
2
|
import { useLatestRef } from './useLatestRef';
|
|
3
|
+
import { applyRangeRowSelection, computeRowSelectionState } from '../utils';
|
|
3
4
|
/**
|
|
4
5
|
* Manages row selection state for single or multiple selection modes with shift-click range support.
|
|
5
6
|
* @param params - Items, getRowId, selection mode, controlled state, and selection change callback.
|
|
@@ -33,22 +34,13 @@ export function useRowSelection(params) {
|
|
|
33
34
|
lastClickedRowRef.current = rowIndex;
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
|
-
const next = new Set(selectedRowIdsRef.current);
|
|
37
37
|
const currentItems = itemsRef.current;
|
|
38
|
+
let next;
|
|
38
39
|
if (shiftKey && lastClickedRowRef.current >= 0 && lastClickedRowRef.current !== rowIndex) {
|
|
39
|
-
|
|
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
|
-
}
|
|
40
|
+
next = applyRangeRowSelection(lastClickedRowRef.current, rowIndex, checked, currentItems, getRowId, selectedRowIdsRef.current);
|
|
50
41
|
}
|
|
51
42
|
else {
|
|
43
|
+
next = new Set(selectedRowIdsRef.current);
|
|
52
44
|
if (checked)
|
|
53
45
|
next.add(rowId);
|
|
54
46
|
else
|
|
@@ -56,9 +48,7 @@ export function useRowSelection(params) {
|
|
|
56
48
|
}
|
|
57
49
|
lastClickedRowRef.current = rowIndex;
|
|
58
50
|
updateSelection(next);
|
|
59
|
-
},
|
|
60
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- itemsRef, selectedRowIdsRef are stable refs
|
|
61
|
-
[rowSelection, getRowId, updateSelection]);
|
|
51
|
+
}, [rowSelection, getRowId, updateSelection, itemsRef, selectedRowIdsRef]);
|
|
62
52
|
const handleSelectAll = useCallback((checked) => {
|
|
63
53
|
if (checked) {
|
|
64
54
|
updateSelection(new Set(items.map((item) => getRowId(item))));
|
|
@@ -67,17 +57,7 @@ export function useRowSelection(params) {
|
|
|
67
57
|
updateSelection(new Set());
|
|
68
58
|
}
|
|
69
59
|
}, [items, getRowId, updateSelection]);
|
|
70
|
-
const allSelected = useMemo(() =>
|
|
71
|
-
if (selectedRowIds.size === 0 || items.length === 0)
|
|
72
|
-
return false;
|
|
73
|
-
return items.every((item) => selectedRowIds.has(getRowId(item)));
|
|
74
|
-
}, [items, selectedRowIds, getRowId]);
|
|
75
|
-
const someSelected = useMemo(() => {
|
|
76
|
-
if (allSelected)
|
|
77
|
-
return false;
|
|
78
|
-
// No iteration needed — any selected row means "some" are selected
|
|
79
|
-
return selectedRowIds.size > 0;
|
|
80
|
-
}, [allSelected, selectedRowIds.size]);
|
|
60
|
+
const { allSelected, someSelected } = useMemo(() => computeRowSelectionState(selectedRowIds, items, getRowId), [items, selectedRowIds, getRowId]);
|
|
81
61
|
return {
|
|
82
62
|
selectedRowIds,
|
|
83
63
|
updateSelection,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { getSelectDisplayText } from './useRichSelectState';
|
|
2
3
|
/**
|
|
3
4
|
* Manages select editor state with keyboard navigation (arrow keys, enter, escape).
|
|
4
5
|
* Simpler than useRichSelectState — no search, just a dropdown list.
|
|
@@ -6,11 +7,7 @@ import { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
6
7
|
export function useSelectState(params) {
|
|
7
8
|
const { values, formatValue, initialValue, onCommit, onCancel } = params;
|
|
8
9
|
const dropdownRef = useRef(null);
|
|
9
|
-
const getDisplayText = useCallback((value) =>
|
|
10
|
-
if (formatValue)
|
|
11
|
-
return formatValue(value);
|
|
12
|
-
return value != null ? String(value) : '';
|
|
13
|
-
}, [formatValue]);
|
|
10
|
+
const getDisplayText = useCallback((value) => getSelectDisplayText(value, formatValue), [formatValue]);
|
|
14
11
|
// Start highlighted on current value
|
|
15
12
|
const initialIndex = values.findIndex((v) => String(v) === String(initialValue));
|
|
16
13
|
const [highlightedIndex, setHighlightedIndex] = useState(Math.max(initialIndex, 0));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a referentially stable value as long as the comparator considers
|
|
4
|
+
* the new value equal to the previous one. Unlike the broken
|
|
5
|
+
* `useMemo` + `useRef` + `useEffect` pattern, this works correctly because
|
|
6
|
+
* it compares *before* React's own reference check on the dependency array.
|
|
7
|
+
*/
|
|
8
|
+
export function useShallowEqualMemo(value, isEqual) {
|
|
9
|
+
const ref = useRef(value);
|
|
10
|
+
if (!isEqual(value, ref.current)) {
|
|
11
|
+
ref.current = value;
|
|
12
|
+
}
|
|
13
|
+
return ref.current;
|
|
14
|
+
}
|
|
@@ -23,8 +23,7 @@ export function useTableLayout(params) {
|
|
|
23
23
|
ro.observe(el);
|
|
24
24
|
measure();
|
|
25
25
|
return () => ro.disconnect();
|
|
26
|
-
|
|
27
|
-
}, []); // wrapperRef excluded — refs are stable
|
|
26
|
+
}, [wrapperRef]);
|
|
28
27
|
// --- Column sizing overrides state ---
|
|
29
28
|
const [columnSizingOverrides, setColumnSizingOverrides] = useState(() => {
|
|
30
29
|
if (!initialColumnWidths)
|
|
@@ -44,15 +43,8 @@ export function useTableLayout(params) {
|
|
|
44
43
|
useEffect(() => {
|
|
45
44
|
const colIds = new Set(flatColumns.map((c) => c.columnId));
|
|
46
45
|
setColumnSizingOverrides((prev) => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
for (const id of Object.keys(next)) {
|
|
50
|
-
if (!colIds.has(id)) {
|
|
51
|
-
delete next[id];
|
|
52
|
-
changed = true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return changed ? next : prev;
|
|
46
|
+
const kept = Object.fromEntries(Object.entries(prev).filter(([id]) => colIds.has(id)));
|
|
47
|
+
return Object.keys(kept).length !== Object.keys(prev).length ? kept : prev;
|
|
56
48
|
});
|
|
57
49
|
}, [flatColumns]);
|
|
58
50
|
// --- Desired table width calculation ---
|
|
@@ -12,30 +12,36 @@ export function useUndoRedo(params) {
|
|
|
12
12
|
}
|
|
13
13
|
const [historyLength, setHistoryLength] = useState(0);
|
|
14
14
|
const [redoLength, setRedoLength] = useState(0);
|
|
15
|
+
const getStack = useCallback(() => {
|
|
16
|
+
const s = stackRef.current;
|
|
17
|
+
if (!s)
|
|
18
|
+
throw new Error('UndoRedoStack not initialized');
|
|
19
|
+
return s;
|
|
20
|
+
}, []);
|
|
15
21
|
const wrapped = useCallback((event) => {
|
|
16
22
|
if (!onCellValueChanged)
|
|
17
23
|
return;
|
|
18
|
-
const stack =
|
|
24
|
+
const stack = getStack();
|
|
19
25
|
stack.record(event);
|
|
20
26
|
if (!stack.isBatching) {
|
|
21
27
|
setHistoryLength(stack.historyLength);
|
|
22
28
|
setRedoLength(stack.redoLength);
|
|
23
29
|
}
|
|
24
30
|
onCellValueChanged(event);
|
|
25
|
-
}, [onCellValueChanged]);
|
|
31
|
+
}, [onCellValueChanged, getStack]);
|
|
26
32
|
const beginBatch = useCallback(() => {
|
|
27
|
-
|
|
28
|
-
}, []);
|
|
33
|
+
getStack().beginBatch();
|
|
34
|
+
}, [getStack]);
|
|
29
35
|
const endBatch = useCallback(() => {
|
|
30
|
-
const stack =
|
|
36
|
+
const stack = getStack();
|
|
31
37
|
stack.endBatch();
|
|
32
38
|
setHistoryLength(stack.historyLength);
|
|
33
39
|
setRedoLength(stack.redoLength);
|
|
34
|
-
}, []);
|
|
40
|
+
}, [getStack]);
|
|
35
41
|
const undo = useCallback(() => {
|
|
36
42
|
if (!onCellValueChanged)
|
|
37
43
|
return;
|
|
38
|
-
const stack =
|
|
44
|
+
const stack = getStack();
|
|
39
45
|
const lastBatch = stack.undo();
|
|
40
46
|
if (!lastBatch)
|
|
41
47
|
return;
|
|
@@ -50,11 +56,11 @@ export function useUndoRedo(params) {
|
|
|
50
56
|
newValue: ev.oldValue,
|
|
51
57
|
});
|
|
52
58
|
}
|
|
53
|
-
}, [onCellValueChanged]);
|
|
59
|
+
}, [onCellValueChanged, getStack]);
|
|
54
60
|
const redo = useCallback(() => {
|
|
55
61
|
if (!onCellValueChanged)
|
|
56
62
|
return;
|
|
57
|
-
const stack =
|
|
63
|
+
const stack = getStack();
|
|
58
64
|
const nextBatch = stack.redo();
|
|
59
65
|
if (!nextBatch)
|
|
60
66
|
return;
|
|
@@ -64,7 +70,7 @@ export function useUndoRedo(params) {
|
|
|
64
70
|
for (const ev of nextBatch) {
|
|
65
71
|
onCellValueChanged(ev);
|
|
66
72
|
}
|
|
67
|
-
}, [onCellValueChanged]);
|
|
73
|
+
}, [onCellValueChanged, getStack]);
|
|
68
74
|
return {
|
|
69
75
|
onCellValueChanged: onCellValueChanged ? wrapped : undefined,
|
|
70
76
|
undo,
|
package/dist/esm/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
3
3
|
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
4
4
|
// Hooks
|
|
5
|
-
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useListVirtualizer, useLatestRef, usePaginationControls, useDataGridTableOrchestration, } from './hooks';
|
|
5
|
+
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useListVirtualizer, useLatestRef, usePaginationControls, useDataGridTableOrchestration, useColumnMeta, } from './hooks';
|
|
6
6
|
// Constants
|
|
7
7
|
export { GRID_ROOT_STYLE, CURSOR_CELL_STYLE, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, NOOP, STOP_PROPAGATION, } from './constants/domHelpers';
|
|
8
8
|
// Components
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Shared utilities re-exported from core
|
|
2
|
-
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from '@alaarab/ogrid-core';
|
|
2
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, applyFillValues, computeArrowNavigation, applyCellDeletion, applyRangeRowSelection, computeRowSelectionState, UndoRedoStack, } from '@alaarab/ogrid-core';
|
|
3
3
|
// View model utilities (re-exported from core + React-specific getCellInteractionProps)
|
|
4
4
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
|
|
5
5
|
export { areGridRowPropsEqual, isRowInRange } from './gridRowComparator';
|
|
@@ -8,5 +8,7 @@ export interface IColumnChooserProps {
|
|
|
8
8
|
columns: IColumnDefinition[];
|
|
9
9
|
visibleColumns: Set<string>;
|
|
10
10
|
onVisibilityChange: (columnKey: string, visible: boolean) => void;
|
|
11
|
+
/** Optional batch setter — used by select-all / clear-all for a single state update. */
|
|
12
|
+
onSetVisibleColumns?: (columns: Set<string>) => void;
|
|
11
13
|
className?: string;
|
|
12
14
|
}
|
|
@@ -44,8 +44,6 @@ export interface DateFilterClassNames {
|
|
|
44
44
|
export declare const DateFilterContent: React.FC<DateFilterContentProps>;
|
|
45
45
|
export declare function getColumnHeaderFilterStateParams(props: IColumnHeaderFilterProps): {
|
|
46
46
|
filterType: ColumnFilterType;
|
|
47
|
-
isSorted: boolean;
|
|
48
|
-
isSortedDescending: boolean;
|
|
49
47
|
onSort: (() => void) | undefined;
|
|
50
48
|
selectedValues: string[] | undefined;
|
|
51
49
|
onFilterChange: ((values: string[]) => void) | undefined;
|
|
@@ -2,6 +2,14 @@ export { useFilterOptions } from './useFilterOptions';
|
|
|
2
2
|
export type { UseFilterOptionsResult } from './useFilterOptions';
|
|
3
3
|
export { useOGrid } from './useOGrid';
|
|
4
4
|
export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, } from './useOGrid';
|
|
5
|
+
export { useOGridPagination } from './useOGridPagination';
|
|
6
|
+
export type { UseOGridPaginationParams, UseOGridPaginationState } from './useOGridPagination';
|
|
7
|
+
export { useOGridSorting } from './useOGridSorting';
|
|
8
|
+
export type { UseOGridSortingParams, UseOGridSortingState, SortState } from './useOGridSorting';
|
|
9
|
+
export { useOGridFilters as useOGridFiltersState } from './useOGridFilters';
|
|
10
|
+
export type { UseOGridFiltersParams, UseOGridFiltersState } from './useOGridFilters';
|
|
11
|
+
export { useOGridDataFetching } from './useOGridDataFetching';
|
|
12
|
+
export type { UseOGridDataFetchingParams, UseOGridDataFetchingState } from './useOGridDataFetching';
|
|
5
13
|
export { useActiveCell } from './useActiveCell';
|
|
6
14
|
export type { UseActiveCellResult } from './useActiveCell';
|
|
7
15
|
export { useCellEditing } from './useCellEditing';
|
|
@@ -23,6 +31,14 @@ export { useFillHandle } from './useFillHandle';
|
|
|
23
31
|
export type { UseFillHandleResult, UseFillHandleParams } from './useFillHandle';
|
|
24
32
|
export { useDataGridState } from './useDataGridState';
|
|
25
33
|
export type { UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, } from './useDataGridState';
|
|
34
|
+
export { useDataGridLayout } from './useDataGridLayout';
|
|
35
|
+
export type { UseDataGridLayoutParams, UseDataGridLayoutResult } from './useDataGridLayout';
|
|
36
|
+
export { useDataGridEditing } from './useDataGridEditing';
|
|
37
|
+
export type { UseDataGridEditingParams, UseDataGridEditingResult } from './useDataGridEditing';
|
|
38
|
+
export { useDataGridInteraction } from './useDataGridInteraction';
|
|
39
|
+
export type { UseDataGridInteractionParams, UseDataGridInteractionResult } from './useDataGridInteraction';
|
|
40
|
+
export { useDataGridContextMenu } from './useDataGridContextMenu';
|
|
41
|
+
export type { UseDataGridContextMenuParams, UseDataGridContextMenuResult } from './useDataGridContextMenu';
|
|
26
42
|
export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
|
|
27
43
|
export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
|
|
28
44
|
export { useTextFilterState } from './useTextFilterState';
|
|
@@ -58,7 +74,10 @@ export type { IVirtualScrollConfig, UseVirtualScrollParams, UseVirtualScrollResu
|
|
|
58
74
|
export { useListVirtualizer } from './useListVirtualizer';
|
|
59
75
|
export type { UseListVirtualizerOptions, UseListVirtualizerResult, VirtualItem, } from './useListVirtualizer';
|
|
60
76
|
export { useLatestRef } from './useLatestRef';
|
|
77
|
+
export { useShallowEqualMemo } from './useShallowEqualMemo';
|
|
61
78
|
export { usePaginationControls } from './usePaginationControls';
|
|
62
79
|
export type { UsePaginationControlsProps, UsePaginationControlsResult, } from './usePaginationControls';
|
|
63
80
|
export { useDataGridTableOrchestration } from './useDataGridTableOrchestration';
|
|
64
81
|
export type { UseDataGridTableOrchestrationParams, UseDataGridTableOrchestrationResult, } from './useDataGridTableOrchestration';
|
|
82
|
+
export { useColumnMeta } from './useColumnMeta';
|
|
83
|
+
export type { UseColumnMetaParams, ColumnMetaResult } from './useColumnMeta';
|
|
@@ -14,7 +14,6 @@ export interface UseClipboardResult {
|
|
|
14
14
|
handleCopy: () => void;
|
|
15
15
|
handleCut: () => void;
|
|
16
16
|
handlePaste: () => Promise<void>;
|
|
17
|
-
cutRangeRef: React.MutableRefObject<ISelectionRange | null>;
|
|
18
17
|
/** Current cut range for UI (marching ants). Null when no cut or after paste. */
|
|
19
18
|
cutRange: ISelectionRange | null;
|
|
20
19
|
/** Current copy range for UI (marching ants). Null when no copy or after paste/cut. */
|
|
@@ -7,6 +7,8 @@ export interface UseColumnChooserStateParams {
|
|
|
7
7
|
columns: IColumnDefinition[];
|
|
8
8
|
visibleColumns: Set<string>;
|
|
9
9
|
onVisibilityChange: (columnKey: string, visible: boolean) => void;
|
|
10
|
+
/** Optional batch setter for select-all / clear-all — avoids N individual callbacks. */
|
|
11
|
+
onSetVisibleColumns?: (columns: Set<string>) => void;
|
|
10
12
|
}
|
|
11
13
|
export interface UseColumnChooserStateResult {
|
|
12
14
|
open: boolean;
|
|
@@ -8,8 +8,6 @@ import type { ColumnFilterType, IDateFilterValue } from '../types/columnTypes';
|
|
|
8
8
|
import type { UserLike } from '../types/dataGridTypes';
|
|
9
9
|
export interface UseColumnHeaderFilterStateParams {
|
|
10
10
|
filterType: ColumnFilterType;
|
|
11
|
-
isSorted?: boolean;
|
|
12
|
-
isSortedDescending?: boolean;
|
|
13
11
|
onSort?: () => void;
|
|
14
12
|
selectedValues?: string[];
|
|
15
13
|
onFilterChange?: (values: string[]) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook that pre-computes per-column styles and class names for DataGridTable.
|
|
3
|
+
* Extracted from Radix/Fluent/Material DataGridTable to avoid duplication.
|
|
4
|
+
*
|
|
5
|
+
* @param params.addStickyPosition - When true, adds `position: 'sticky'` inline for pinned columns.
|
|
6
|
+
* This is needed by Fluent UI whose `TableCell` injects atomic `position: relative` via CSS-in-JS,
|
|
7
|
+
* overriding the shared `.pinnedColLeft { position: sticky }` class. Radix/Material don't need it.
|
|
8
|
+
*/
|
|
9
|
+
import type { IColumnDef } from '../types';
|
|
10
|
+
export interface UseColumnMetaParams<T> {
|
|
11
|
+
visibleCols: IColumnDef<T>[];
|
|
12
|
+
getColumnWidth: (col: IColumnDef<T>) => number;
|
|
13
|
+
columnSizingOverrides: Record<string, {
|
|
14
|
+
widthPx: number;
|
|
15
|
+
}>;
|
|
16
|
+
measuredColumnWidths: Record<string, number>;
|
|
17
|
+
pinnedColumns: Record<string, 'left' | 'right'>;
|
|
18
|
+
leftOffsets: Record<string, number>;
|
|
19
|
+
rightOffsets: Record<string, number>;
|
|
20
|
+
pinnedColLeftClass: string;
|
|
21
|
+
pinnedColRightClass: string;
|
|
22
|
+
/** When true, adds `position: sticky` inline to pinned cells (Fluent-specific). */
|
|
23
|
+
addStickyPosition?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ColumnMetaResult {
|
|
26
|
+
cellStyles: Record<string, React.CSSProperties>;
|
|
27
|
+
cellClasses: Record<string, string>;
|
|
28
|
+
hdrStyles: Record<string, React.CSSProperties>;
|
|
29
|
+
hdrClasses: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Computes per-column styles and class names once per render, avoiding per-cell object creation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useColumnMeta<T>(params: UseColumnMetaParams<T>): ColumnMetaResult;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DataGridContextMenuState } from './useDataGridState';
|
|
2
|
+
export interface UseDataGridContextMenuParams {
|
|
3
|
+
cellSelection: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface UseDataGridContextMenuResult {
|
|
6
|
+
contextMenu: DataGridContextMenuState;
|
|
7
|
+
contextMenuPosition: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
} | null;
|
|
11
|
+
setContextMenuPosition: (pos: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
} | null) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Manages context menu position and handlers.
|
|
18
|
+
* Extracted from useDataGridState for modularity.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useDataGridContextMenu(params: UseDataGridContextMenuParams): UseDataGridContextMenuResult;
|