@alaarab/ogrid-core 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/esm/components/OGridLayout.js +7 -7
- package/dist/esm/components/SideBar.js +14 -12
- package/dist/esm/hooks/useColumnResize.js +10 -4
- package/dist/esm/hooks/useDataGridState.js +75 -73
- package/dist/esm/hooks/useOGrid.js +16 -117
- package/dist/esm/hooks/useRowSelection.js +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/types/dataGridTypes.js +0 -24
- package/dist/esm/types/index.js +1 -1
- package/dist/esm/utils/clientSideData.js +94 -0
- package/dist/esm/utils/dataGridViewModel.js +9 -14
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/ogridHelpers.js +5 -7
- package/dist/types/components/OGridLayout.d.ts +2 -7
- package/dist/types/components/SideBar.d.ts +3 -7
- package/dist/types/hooks/index.d.ts +1 -1
- package/dist/types/hooks/useDataGridState.d.ts +63 -39
- package/dist/types/hooks/useOGrid.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/types/dataGridTypes.d.ts +21 -20
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/clientSideData.d.ts +13 -0
- package/dist/types/utils/dataGridViewModel.d.ts +4 -10
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/ogridHelpers.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,14 +32,13 @@ npm install @alaarab/ogrid-core
|
|
|
32
32
|
|
|
33
33
|
### Components
|
|
34
34
|
|
|
35
|
-
- `OGridLayout` --
|
|
35
|
+
- `OGridLayout` -- Unified bordered layout: toolbar strip (custom content + column chooser), optional secondary toolbar row, sidebar, grid area, and footer strip (pagination).
|
|
36
36
|
|
|
37
37
|
### Utilities
|
|
38
38
|
|
|
39
39
|
- `getPaginationViewModel(...)` -- Page numbers, ellipsis, start/end item for PaginationControls
|
|
40
40
|
- `getHeaderFilterConfig(col, input)` -- ColumnHeaderFilter props from column + filter/sort state
|
|
41
41
|
- `getCellRenderDescriptor(item, col, rowIndex, colIdx, input)` -- Cell mode (editing-inline / editing-popover / display) and flags for DataGridTable
|
|
42
|
-
- `toDataGridFilterProps(filters)` -- Splits `IFilters` into `multiSelectFilters`, `textFilters`, `peopleFilters`
|
|
43
42
|
- `toUserLike(user)` -- Converts a user-like object to `UserLike`
|
|
44
43
|
- `exportToCsv(items, columns, getValue, filename)` -- Full CSV export
|
|
45
44
|
- `buildCsvHeader`, `buildCsvRows`, `triggerCsvDownload`, `escapeCsvValue` -- Low-level CSV helpers
|
|
@@ -27,6 +27,11 @@ const toolbarSectionStyle = {
|
|
|
27
27
|
alignItems: 'center',
|
|
28
28
|
gap: 8,
|
|
29
29
|
};
|
|
30
|
+
const toolbarBelowStyle = {
|
|
31
|
+
padding: '6px 12px',
|
|
32
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
33
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
34
|
+
};
|
|
30
35
|
const footerStripStyle = {
|
|
31
36
|
borderTop: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
32
37
|
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
@@ -56,7 +61,6 @@ const gridChildStyle = {
|
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
58
63
|
* Renders OGrid layout as a unified bordered container:
|
|
59
|
-
* [deprecated title above]
|
|
60
64
|
* ┌────────────────────────────────────┐
|
|
61
65
|
* │ [toolbar strip] │
|
|
62
66
|
* ├────────────────────────────────────┤
|
|
@@ -66,18 +70,14 @@ const gridChildStyle = {
|
|
|
66
70
|
* └────────────────────────────────────┘
|
|
67
71
|
*/
|
|
68
72
|
export function OGridLayout(props) {
|
|
69
|
-
const { containerComponent: Container = 'div', containerProps = {},
|
|
73
|
+
const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, toolbarBelow, children, pagination, sideBar, } = props;
|
|
70
74
|
const hasSideBar = sideBar != null;
|
|
71
75
|
const sideBarPosition = sideBar?.position ?? 'right';
|
|
72
|
-
// Backward compat: columnChooser prop → toolbarEnd
|
|
73
|
-
const toolbarEnd = toolbarEndProp ?? columnChooser;
|
|
74
76
|
const hasToolbar = toolbar != null || toolbarEnd != null;
|
|
75
|
-
// Root styles: flex column, fill parent height, gap for deprecated title spacing
|
|
76
77
|
const rootStyle = {
|
|
77
78
|
display: 'flex',
|
|
78
79
|
flexDirection: 'column',
|
|
79
80
|
height: '100%',
|
|
80
|
-
gap: title != null ? (typeof gap === 'number' ? `${gap}px` : gap) : undefined,
|
|
81
81
|
};
|
|
82
|
-
return (
|
|
82
|
+
return (_jsx(Container, { className: className, style: rootStyle, ...containerProps, children: _jsxs("div", { style: borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsx("div", { style: toolbarSectionStyle, children: toolbarEnd })] })), toolbarBelow && (_jsx("div", { style: toolbarBelowStyle, children: toolbarBelow })), hasSideBar ? (_jsxs("div", { style: gridAreaFlexStyle, children: [sideBarPosition === 'left' && _jsx(SideBar, { ...sideBar }), _jsx("div", { style: gridChildStyle, children: children }), sideBarPosition !== 'left' && _jsx(SideBar, { ...sideBar })] })) : (_jsx("div", { style: gridAreaSoloStyle, children: children })), pagination && (_jsx("div", { style: footerStripStyle, children: pagination }))] }) }));
|
|
83
83
|
}
|
|
@@ -6,7 +6,7 @@ const PANEL_LABELS = {
|
|
|
6
6
|
filters: 'Filters',
|
|
7
7
|
};
|
|
8
8
|
export function SideBar(props) {
|
|
9
|
-
const { activePanel, onPanelChange, panels, position, columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, filterableColumns,
|
|
9
|
+
const { activePanel, onPanelChange, panels, position, columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, filterableColumns, filters, onFilterChange, filterOptions, } = props;
|
|
10
10
|
const isOpen = activePanel !== null;
|
|
11
11
|
const handleTabClick = (panel) => {
|
|
12
12
|
onPanelChange(activePanel === panel ? null : panel);
|
|
@@ -47,7 +47,7 @@ export function SideBar(props) {
|
|
|
47
47
|
padding: '8px 12px',
|
|
48
48
|
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
49
49
|
fontWeight: 600,
|
|
50
|
-
}, children: [_jsx("span", { children: PANEL_LABELS[activePanel] }), _jsx("button", { onClick: () => onPanelChange(null), style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16, color: 'var(--ogrid-fg, #242424)' }, "aria-label": "Close panel", children: "\u00D7" })] }), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '8px 12px' }, children: [activePanel === 'columns' && (_jsx(ColumnsPanel, { columns: columns, visibleColumns: visibleColumns, onVisibilityChange: onVisibilityChange, onSetVisibleColumns: onSetVisibleColumns })), activePanel === 'filters' && (_jsx(FiltersPanel, { filterableColumns: filterableColumns,
|
|
50
|
+
}, children: [_jsx("span", { children: PANEL_LABELS[activePanel] }), _jsx("button", { onClick: () => onPanelChange(null), style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16, color: 'var(--ogrid-fg, #242424)' }, "aria-label": "Close panel", children: "\u00D7" })] }), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '8px 12px' }, children: [activePanel === 'columns' && (_jsx(ColumnsPanel, { columns: columns, visibleColumns: visibleColumns, onVisibilityChange: onVisibilityChange, onSetVisibleColumns: onSetVisibleColumns })), activePanel === 'filters' && (_jsx(FiltersPanel, { filterableColumns: filterableColumns, filters: filters, onFilterChange: onFilterChange, filterOptions: filterOptions }))] })] })) : null;
|
|
51
51
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', flexShrink: 0 }, role: "complementary", "aria-label": "Side bar", children: [position === 'left' && tabStrip, position === 'left' && panelContent, position === 'right' && panelContent, position === 'right' && tabStrip] }));
|
|
52
52
|
}
|
|
53
53
|
// --- Internal sub-components ---
|
|
@@ -70,28 +70,30 @@ function ColumnsPanel(props) {
|
|
|
70
70
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'flex', gap: 8, marginBottom: 8 }, children: [_jsx("button", { onClick: handleSelectAll, disabled: allVisible, style: { flex: 1, cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4, padding: '4px 8px' }, children: "Select All" }), _jsx("button", { onClick: handleClearAll, style: { flex: 1, cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4, padding: '4px 8px' }, children: "Clear All" })] }), columns.map((col) => (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0', cursor: 'pointer' }, children: [_jsx("input", { type: "checkbox", checked: visibleColumns.has(col.columnId), onChange: (e) => onVisibilityChange(col.columnId, e.target.checked), disabled: col.required }), _jsx("span", { children: col.name })] }, col.columnId)))] }));
|
|
71
71
|
}
|
|
72
72
|
function FiltersPanel(props) {
|
|
73
|
-
const { filterableColumns,
|
|
73
|
+
const { filterableColumns, filters, onFilterChange, filterOptions } = props;
|
|
74
74
|
if (filterableColumns.length === 0) {
|
|
75
75
|
return _jsx("div", { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' }, children: "No filterable columns" });
|
|
76
76
|
}
|
|
77
77
|
return (_jsx(_Fragment, { children: filterableColumns.map((col) => {
|
|
78
78
|
const filterKey = col.filterField;
|
|
79
|
-
return (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 500, marginBottom: 4, fontSize: 13 }, children: col.name }), col.filterType === 'text' && (_jsx("input", { type: "text", value:
|
|
79
|
+
return (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 500, marginBottom: 4, fontSize: 13 }, children: col.name }), col.filterType === 'text' && (_jsx("input", { type: "text", value: filters[filterKey]?.type === 'text' ? filters[filterKey].value : '', onChange: (e) => onFilterChange(filterKey, e.target.value ? { type: 'text', value: e.target.value } : undefined), placeholder: `Filter ${col.name}...`, "aria-label": `Filter ${col.name}`, style: { width: '100%', boxSizing: 'border-box', padding: '4px 6px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })), col.filterType === 'date' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.from ?? '') : '', onChange: (e) => {
|
|
80
80
|
const from = e.target.value || undefined;
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
|
|
82
|
+
const to = existingValue.to;
|
|
83
|
+
onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
84
|
+
}, "aria-label": `${col.name} from date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: filters[filterKey]?.type === 'date' ? (filters[filterKey].value.to ?? '') : '', onChange: (e) => {
|
|
84
85
|
const to = e.target.value || undefined;
|
|
85
|
-
const
|
|
86
|
-
|
|
86
|
+
const existingValue = filters[filterKey]?.type === 'date' ? filters[filterKey].value : {};
|
|
87
|
+
const from = existingValue.from;
|
|
88
|
+
onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
|
|
87
89
|
}, "aria-label": `${col.name} to date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] })] })), col.filterType === 'multiSelect' && (_jsx("div", { style: { maxHeight: 120, overflowY: 'auto' }, role: "group", "aria-label": `${col.name} options`, children: (filterOptions[filterKey] ?? []).map((opt) => {
|
|
88
|
-
const selected =
|
|
90
|
+
const selected = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value.includes(opt) : false;
|
|
89
91
|
return (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, padding: '1px 0', cursor: 'pointer', fontSize: 13 }, children: [_jsx("input", { type: "checkbox", checked: selected, onChange: (e) => {
|
|
90
|
-
const current =
|
|
92
|
+
const current = filters[filterKey]?.type === 'multiSelect' ? filters[filterKey].value : [];
|
|
91
93
|
const next = e.target.checked
|
|
92
94
|
? [...current, opt]
|
|
93
95
|
: current.filter((v) => v !== opt);
|
|
94
|
-
|
|
96
|
+
onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
|
|
95
97
|
} }), _jsx("span", { children: opt })] }, opt));
|
|
96
98
|
}) }))] }, col.columnId));
|
|
97
99
|
}) }));
|
|
@@ -8,10 +8,16 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
8
8
|
e.stopPropagation();
|
|
9
9
|
const startX = e.clientX;
|
|
10
10
|
const columnId = col.columnId;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Measure the actual rendered width from the DOM. With table-layout: auto,
|
|
12
|
+
// the browser may have auto-sized the column wider than the config values.
|
|
13
|
+
// The resize handle is a direct child of <th>, so parentElement is the header cell.
|
|
14
|
+
const thEl = e.currentTarget.parentElement;
|
|
15
|
+
const startWidth = thEl
|
|
16
|
+
? thEl.getBoundingClientRect().width
|
|
17
|
+
: columnSizingOverrides[columnId]?.widthPx
|
|
18
|
+
?? col.idealWidth
|
|
19
|
+
?? col.defaultWidth
|
|
20
|
+
?? defaultWidth;
|
|
15
21
|
let latestWidth = startWidth;
|
|
16
22
|
// Lock cursor and prevent text selection during drag
|
|
17
23
|
const prevCursor = document.body.style.cursor;
|
|
@@ -99,6 +99,8 @@ export function useDataGridState(params) {
|
|
|
99
99
|
endBatch: undoRedo.endBatch,
|
|
100
100
|
});
|
|
101
101
|
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
102
|
+
if (e.button !== 0)
|
|
103
|
+
return;
|
|
102
104
|
wrapperRef.current?.focus();
|
|
103
105
|
clearClipboardRanges();
|
|
104
106
|
handleCellMouseDownBase(e, rowIndex, globalColIndex);
|
|
@@ -209,37 +211,25 @@ export function useDataGridState(params) {
|
|
|
209
211
|
const showEmptyInGrid = items.length === 0 && !!emptyState;
|
|
210
212
|
const hasCellSelection = selectionRange != null || activeCell != null;
|
|
211
213
|
// --- View-model inputs (shared across all 3 DataGridTables) ---
|
|
212
|
-
const { sortBy, sortDirection, onColumnSort,
|
|
214
|
+
const { sortBy, sortDirection, onColumnSort, filters, onFilterChange, filterOptions, loadingFilterOptions, peopleSearch, } = props;
|
|
213
215
|
const headerFilterInput = useMemo(() => ({
|
|
214
216
|
sortBy,
|
|
215
217
|
sortDirection,
|
|
216
218
|
onColumnSort,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
peopleFilters,
|
|
220
|
-
onPeopleFilterChange,
|
|
221
|
-
peopleSearch,
|
|
219
|
+
filters,
|
|
220
|
+
onFilterChange,
|
|
222
221
|
filterOptions,
|
|
223
222
|
loadingFilterOptions,
|
|
224
|
-
|
|
225
|
-
onMultiSelectFilterChange,
|
|
226
|
-
dateFilters,
|
|
227
|
-
onDateFilterChange,
|
|
223
|
+
peopleSearch,
|
|
228
224
|
}), [
|
|
229
225
|
sortBy,
|
|
230
226
|
sortDirection,
|
|
231
227
|
onColumnSort,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
peopleFilters,
|
|
235
|
-
onPeopleFilterChange,
|
|
236
|
-
peopleSearch,
|
|
228
|
+
filters,
|
|
229
|
+
onFilterChange,
|
|
237
230
|
filterOptions,
|
|
238
231
|
loadingFilterOptions,
|
|
239
|
-
|
|
240
|
-
onMultiSelectFilterChange,
|
|
241
|
-
dateFilters,
|
|
242
|
-
onDateFilterChange,
|
|
232
|
+
peopleSearch,
|
|
243
233
|
]);
|
|
244
234
|
const cellDescriptorInput = useMemo(() => ({
|
|
245
235
|
editingCell,
|
|
@@ -305,59 +295,71 @@ export function useDataGridState(params) {
|
|
|
305
295
|
setPendingEditorValue(undefined);
|
|
306
296
|
}, [setEditingCell, setPendingEditorValue]);
|
|
307
297
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
298
|
+
layout: {
|
|
299
|
+
flatColumns,
|
|
300
|
+
visibleCols,
|
|
301
|
+
visibleColumnCount,
|
|
302
|
+
totalColCount,
|
|
303
|
+
colOffset,
|
|
304
|
+
hasCheckboxCol,
|
|
305
|
+
rowIndexByRowId,
|
|
306
|
+
containerWidth,
|
|
307
|
+
minTableWidth,
|
|
308
|
+
desiredTableWidth,
|
|
309
|
+
columnSizingOverrides,
|
|
310
|
+
setColumnSizingOverrides,
|
|
311
|
+
onColumnResized,
|
|
312
|
+
},
|
|
313
|
+
rowSelection: {
|
|
314
|
+
selectedRowIds,
|
|
315
|
+
updateSelection,
|
|
316
|
+
handleRowCheckboxChange,
|
|
317
|
+
handleSelectAll,
|
|
318
|
+
allSelected,
|
|
319
|
+
someSelected,
|
|
320
|
+
},
|
|
321
|
+
editing: {
|
|
322
|
+
editingCell,
|
|
323
|
+
setEditingCell,
|
|
324
|
+
pendingEditorValue,
|
|
325
|
+
setPendingEditorValue,
|
|
326
|
+
commitCellEdit,
|
|
327
|
+
cancelPopoverEdit,
|
|
328
|
+
popoverAnchorEl,
|
|
329
|
+
setPopoverAnchorEl,
|
|
330
|
+
},
|
|
331
|
+
interaction: {
|
|
332
|
+
activeCell: cellSelection ? activeCell : null,
|
|
333
|
+
setActiveCell: cellSelection ? setActiveCell : NOOP,
|
|
334
|
+
selectionRange: cellSelection ? selectionRange : null,
|
|
335
|
+
setSelectionRange: cellSelection ? setSelectionRange : NOOP,
|
|
336
|
+
handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
|
|
337
|
+
handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
|
|
338
|
+
hasCellSelection: cellSelection ? hasCellSelection : false,
|
|
339
|
+
handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
|
|
340
|
+
handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
|
|
341
|
+
handleCopy: cellSelection ? handleCopy : NOOP,
|
|
342
|
+
handleCut: cellSelection ? handleCut : NOOP,
|
|
343
|
+
handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
|
|
344
|
+
cutRange: cellSelection ? cutRange : null,
|
|
345
|
+
copyRange: cellSelection ? copyRange : null,
|
|
346
|
+
clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
|
|
347
|
+
canUndo: undoRedo.canUndo,
|
|
348
|
+
canRedo: undoRedo.canRedo,
|
|
349
|
+
onUndo: undoRedo.undo,
|
|
350
|
+
onRedo: undoRedo.redo,
|
|
351
|
+
},
|
|
352
|
+
contextMenu: {
|
|
353
|
+
menuPosition: cellSelection ? contextMenu : null,
|
|
354
|
+
setMenuPosition: cellSelection ? setContextMenu : NOOP,
|
|
355
|
+
handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
|
|
356
|
+
closeContextMenu: cellSelection ? closeContextMenu : NOOP,
|
|
357
|
+
},
|
|
358
|
+
viewModels: {
|
|
359
|
+
headerFilterInput,
|
|
360
|
+
cellDescriptorInput,
|
|
361
|
+
statusBarConfig,
|
|
362
|
+
showEmptyInGrid,
|
|
363
|
+
},
|
|
362
364
|
};
|
|
363
365
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useMemo, useCallback, useState, useEffect, useRef, useImperativeHandle, } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { toDataGridFilterProps } from '../types';
|
|
2
|
+
import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from '../utils/ogridHelpers';
|
|
3
|
+
import { flattenColumns, processClientSideData } from '../utils';
|
|
5
4
|
import { useFilterOptions } from './useFilterOptions';
|
|
6
5
|
import { useSideBarState } from './useSideBarState';
|
|
7
6
|
const DEFAULT_PAGE_SIZE = 25;
|
|
8
7
|
export function useOGrid(props, ref) {
|
|
9
|
-
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, emptyState, entityLabelPlural = 'items', className,
|
|
8
|
+
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;
|
|
10
9
|
// Resolve column chooser placement
|
|
11
10
|
const columnChooserPlacement = columnChooserProp === false ? 'none'
|
|
12
11
|
: columnChooserProp === 'sidebar' ? 'sidebar'
|
|
@@ -70,23 +69,14 @@ export function useOGrid(props, ref) {
|
|
|
70
69
|
setInternalVisibleColumns(cols);
|
|
71
70
|
onVisibleColumnsChange?.(cols);
|
|
72
71
|
}, [controlledVisibleColumns, onVisibleColumnsChange]);
|
|
73
|
-
const { multiSelectFilters, textFilters, peopleFilters, dateFilters } = useMemo(() => toDataGridFilterProps(filters), [filters]);
|
|
74
72
|
const handleSort = useCallback((columnKey) => {
|
|
75
73
|
setSort({
|
|
76
74
|
field: columnKey,
|
|
77
75
|
direction: sort.field === columnKey && sort.direction === 'asc' ? 'desc' : 'asc',
|
|
78
76
|
});
|
|
79
77
|
}, [sort, setSort]);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}, [filters, setFilters]);
|
|
83
|
-
const handleTextFilterChange = useCallback((key, value) => {
|
|
84
|
-
setFilters(mergeFilter(filters, key, value.trim() || undefined));
|
|
85
|
-
}, [filters, setFilters]);
|
|
86
|
-
const handlePeopleFilterChange = useCallback((key, user) => {
|
|
87
|
-
setFilters(mergeFilter(filters, key, user ?? undefined));
|
|
88
|
-
}, [filters, setFilters]);
|
|
89
|
-
const handleDateFilterChange = useCallback((key, value) => {
|
|
78
|
+
/** Single filter change handler — wraps discriminated FilterValue into mergeFilter. */
|
|
79
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
90
80
|
setFilters(mergeFilter(filters, key, value));
|
|
91
81
|
}, [filters, setFilters]);
|
|
92
82
|
const handleVisibilityChange = useCallback((columnKey, isVisible) => {
|
|
@@ -113,88 +103,11 @@ export function useOGrid(props, ref) {
|
|
|
113
103
|
return serverFilterOptions;
|
|
114
104
|
return deriveFilterOptionsFromData(displayData, columns);
|
|
115
105
|
}, [dataSource, displayData, columns, serverFilterOptions]);
|
|
106
|
+
// --- Client-side filtering & sorting ---
|
|
116
107
|
const clientItemsAndTotal = useMemo(() => {
|
|
117
108
|
if (!isClientSide)
|
|
118
109
|
return null;
|
|
119
|
-
|
|
120
|
-
columns.forEach((col) => {
|
|
121
|
-
const filterKey = getFilterField(col);
|
|
122
|
-
const f = col.filterable && typeof col.filterable === 'object'
|
|
123
|
-
? col.filterable
|
|
124
|
-
: null;
|
|
125
|
-
const type = f?.type;
|
|
126
|
-
const val = filters[filterKey];
|
|
127
|
-
if (type === 'multiSelect' && Array.isArray(val) && val.length > 0) {
|
|
128
|
-
rows = rows.filter((r) => val.includes(String(getCellValue(r, col))));
|
|
129
|
-
}
|
|
130
|
-
else if (type === 'text' &&
|
|
131
|
-
typeof val === 'string' &&
|
|
132
|
-
val.trim()) {
|
|
133
|
-
const lower = val.trim().toLowerCase();
|
|
134
|
-
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase().includes(lower));
|
|
135
|
-
}
|
|
136
|
-
else if (type === 'people' &&
|
|
137
|
-
val &&
|
|
138
|
-
typeof val === 'object' &&
|
|
139
|
-
'email' in val) {
|
|
140
|
-
const email = val.email.toLowerCase();
|
|
141
|
-
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase() === email);
|
|
142
|
-
}
|
|
143
|
-
else if (type === 'date' &&
|
|
144
|
-
val &&
|
|
145
|
-
typeof val === 'object' &&
|
|
146
|
-
!Array.isArray(val) &&
|
|
147
|
-
('from' in val || 'to' in val)) {
|
|
148
|
-
const dv = val;
|
|
149
|
-
rows = rows.filter((r) => {
|
|
150
|
-
const cellVal = getCellValue(r, col);
|
|
151
|
-
if (cellVal == null)
|
|
152
|
-
return false;
|
|
153
|
-
const cellDate = new Date(String(cellVal));
|
|
154
|
-
if (Number.isNaN(cellDate.getTime()))
|
|
155
|
-
return false;
|
|
156
|
-
const cellDateStr = cellDate.toISOString().split('T')[0];
|
|
157
|
-
if (dv.from && cellDateStr < dv.from)
|
|
158
|
-
return false;
|
|
159
|
-
if (dv.to && cellDateStr > dv.to)
|
|
160
|
-
return false;
|
|
161
|
-
return true;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
if (sort.field) {
|
|
166
|
-
const sortCol = columns.find((c) => c.columnId === sort.field);
|
|
167
|
-
const compare = sortCol?.compare;
|
|
168
|
-
const dir = sort.direction === 'asc' ? 1 : -1;
|
|
169
|
-
rows.sort((a, b) => {
|
|
170
|
-
if (compare)
|
|
171
|
-
return compare(a, b) * dir;
|
|
172
|
-
const av = sortCol
|
|
173
|
-
? getCellValue(a, sortCol)
|
|
174
|
-
: a[sort.field];
|
|
175
|
-
const bv = sortCol
|
|
176
|
-
? getCellValue(b, sortCol)
|
|
177
|
-
: b[sort.field];
|
|
178
|
-
if (av == null && bv == null)
|
|
179
|
-
return 0;
|
|
180
|
-
if (av == null)
|
|
181
|
-
return -1 * dir;
|
|
182
|
-
if (bv == null)
|
|
183
|
-
return 1 * dir;
|
|
184
|
-
if (sortCol?.type === 'date') {
|
|
185
|
-
const at = new Date(String(av)).getTime();
|
|
186
|
-
const bt = new Date(String(bv)).getTime();
|
|
187
|
-
const aN = Number.isNaN(at) ? 0 : at;
|
|
188
|
-
const bN = Number.isNaN(bt) ? 0 : bt;
|
|
189
|
-
return aN === bN ? 0 : aN > bN ? dir : -dir;
|
|
190
|
-
}
|
|
191
|
-
if (typeof av === 'number' && typeof bv === 'number')
|
|
192
|
-
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
193
|
-
const as = String(av).toLowerCase();
|
|
194
|
-
const bs = String(bv).toLowerCase();
|
|
195
|
-
return as === bs ? 0 : as > bs ? dir : -dir;
|
|
196
|
-
});
|
|
197
|
-
}
|
|
110
|
+
const rows = processClientSideData(displayData, columns, filters, sort.field, sort.direction);
|
|
198
111
|
const total = rows.length;
|
|
199
112
|
const start = (page - 1) * pageSize;
|
|
200
113
|
const paged = rows.slice(start, start + pageSize);
|
|
@@ -344,9 +257,9 @@ export function useOGrid(props, ref) {
|
|
|
344
257
|
getRowId,
|
|
345
258
|
onSelectionChange,
|
|
346
259
|
]);
|
|
260
|
+
// With discriminated union, any defined value is active (mergeFilter already strips empties)
|
|
347
261
|
const hasActiveFilters = useMemo(() => {
|
|
348
|
-
return Object.values(filters).some((v) => v !== undefined
|
|
349
|
-
(Array.isArray(v) ? v.length > 0 : typeof v === 'string' ? v.trim() !== '' : true));
|
|
262
|
+
return Object.values(filters).some((v) => v !== undefined);
|
|
350
263
|
}, [filters]);
|
|
351
264
|
const columnChooserColumns = useMemo(() => columns.map((c) => ({
|
|
352
265
|
columnId: c.columnId,
|
|
@@ -413,12 +326,8 @@ export function useOGrid(props, ref) {
|
|
|
413
326
|
onVisibilityChange: handleVisibilityChange,
|
|
414
327
|
onSetVisibleColumns: setVisibleColumns,
|
|
415
328
|
filterableColumns,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
onMultiSelectFilterChange: handleMultiSelectFilterChange,
|
|
419
|
-
onTextFilterChange: handleTextFilterChange,
|
|
420
|
-
dateFilters,
|
|
421
|
-
onDateFilterChange: handleDateFilterChange,
|
|
329
|
+
filters,
|
|
330
|
+
onFilterChange: handleFilterChange,
|
|
422
331
|
filterOptions: clientFilterOptions,
|
|
423
332
|
};
|
|
424
333
|
}, [
|
|
@@ -432,12 +341,8 @@ export function useOGrid(props, ref) {
|
|
|
432
341
|
handleVisibilityChange,
|
|
433
342
|
setVisibleColumns,
|
|
434
343
|
filterableColumns,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
handleMultiSelectFilterChange,
|
|
438
|
-
handleTextFilterChange,
|
|
439
|
-
dateFilters,
|
|
440
|
-
handleDateFilterChange,
|
|
344
|
+
filters,
|
|
345
|
+
handleFilterChange,
|
|
441
346
|
clientFilterOptions,
|
|
442
347
|
]);
|
|
443
348
|
const dataGridProps = {
|
|
@@ -468,14 +373,8 @@ export function useOGrid(props, ref) {
|
|
|
468
373
|
onSelectionChange: handleSelectionChange,
|
|
469
374
|
statusBar: statusBarConfig,
|
|
470
375
|
isLoading: (isServerSide && loading) || displayLoading,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
textFilters,
|
|
474
|
-
onTextFilterChange: handleTextFilterChange,
|
|
475
|
-
peopleFilters,
|
|
476
|
-
onPeopleFilterChange: handlePeopleFilterChange,
|
|
477
|
-
dateFilters,
|
|
478
|
-
onDateFilterChange: handleDateFilterChange,
|
|
376
|
+
filters,
|
|
377
|
+
onFilterChange: handleFilterChange,
|
|
479
378
|
filterOptions: clientFilterOptions,
|
|
480
379
|
loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : {},
|
|
481
380
|
peopleSearch: dataSource?.searchPeople,
|
|
@@ -502,8 +401,8 @@ export function useOGrid(props, ref) {
|
|
|
502
401
|
visibleColumns,
|
|
503
402
|
handleVisibilityChange,
|
|
504
403
|
columnChooserPlacement,
|
|
505
|
-
title,
|
|
506
404
|
toolbar,
|
|
405
|
+
toolbarBelow,
|
|
507
406
|
className,
|
|
508
407
|
entityLabelPlural,
|
|
509
408
|
emptyState,
|
|
@@ -55,8 +55,8 @@ export function useRowSelection(params) {
|
|
|
55
55
|
updateSelection(new Set());
|
|
56
56
|
}
|
|
57
57
|
}, [items, getRowId, updateSelection]);
|
|
58
|
-
const allSelected = items.length > 0 && items.every((item) => selectedRowIds.has(getRowId(item)));
|
|
59
|
-
const someSelected = !allSelected && items.some((item) => selectedRowIds.has(getRowId(item)));
|
|
58
|
+
const allSelected = useMemo(() => items.length > 0 && items.every((item) => selectedRowIds.has(getRowId(item))), [items, selectedRowIds, getRowId]);
|
|
59
|
+
const someSelected = useMemo(() => !allSelected && items.some((item) => selectedRowIds.has(getRowId(item))), [allSelected, items, selectedRowIds, getRowId]);
|
|
60
60
|
return {
|
|
61
61
|
selectedRowIds,
|
|
62
62
|
updateSelection,
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { toUserLike,
|
|
1
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
2
2
|
// Hooks
|
|
3
3
|
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, } from './hooks';
|
|
4
4
|
// Components
|
|
@@ -8,4 +8,4 @@ export { GridContextMenu } from './components/GridContextMenu';
|
|
|
8
8
|
export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
9
9
|
export { SideBar } from './components/SideBar';
|
|
10
10
|
// Utilities
|
|
11
|
-
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, } from './utils';
|
|
11
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, } from './utils';
|
|
@@ -8,30 +8,6 @@ export function toUserLike(u) {
|
|
|
8
8
|
photo: u.photo
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
/** Type guard for IDateFilterValue. */
|
|
12
|
-
function isDateFilterValue(value) {
|
|
13
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value) && !('email' in value) && ('from' in value || 'to' in value);
|
|
14
|
-
}
|
|
15
|
-
/** Split IFilters into DataGridTable's multiSelect, text, people, and date props. */
|
|
16
|
-
export function toDataGridFilterProps(filters) {
|
|
17
|
-
const multiSelectFilters = {};
|
|
18
|
-
const textFilters = {};
|
|
19
|
-
const peopleFilters = {};
|
|
20
|
-
const dateFilters = {};
|
|
21
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
22
|
-
if (value === undefined)
|
|
23
|
-
continue;
|
|
24
|
-
if (Array.isArray(value))
|
|
25
|
-
multiSelectFilters[key] = value;
|
|
26
|
-
else if (typeof value === 'string')
|
|
27
|
-
textFilters[key] = value;
|
|
28
|
-
else if (typeof value === 'object' && value !== null && 'email' in value)
|
|
29
|
-
peopleFilters[key] = value;
|
|
30
|
-
else if (isDateFilterValue(value))
|
|
31
|
-
dateFilters[key] = value;
|
|
32
|
-
}
|
|
33
|
-
return { multiSelectFilters, textFilters, peopleFilters, dateFilters };
|
|
34
|
-
}
|
|
35
11
|
/** Returns true if (row, col) is inside the range (inclusive). */
|
|
36
12
|
export function isInSelectionRange(range, row, col) {
|
|
37
13
|
const minR = Math.min(range.startRow, range.endRow);
|
package/dist/esm/types/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { toUserLike,
|
|
1
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getCellValue } from './cellValue';
|
|
2
|
+
import { getFilterField } from './ogridHelpers';
|
|
3
|
+
/**
|
|
4
|
+
* Apply client-side filtering and sorting to data.
|
|
5
|
+
* Extracted from useOGrid for testability and reuse.
|
|
6
|
+
*
|
|
7
|
+
* @param data - The full dataset to process
|
|
8
|
+
* @param columns - Column definitions (used for filtering and sorting)
|
|
9
|
+
* @param filters - Current filter state (discriminated FilterValue union)
|
|
10
|
+
* @param sortBy - Column ID to sort by (optional)
|
|
11
|
+
* @param sortDirection - Sort direction (optional)
|
|
12
|
+
* @returns Filtered and sorted array
|
|
13
|
+
*/
|
|
14
|
+
export function processClientSideData(data, columns, filters, sortBy, sortDirection) {
|
|
15
|
+
let rows = data.slice();
|
|
16
|
+
// --- Filtering ---
|
|
17
|
+
columns.forEach((col) => {
|
|
18
|
+
const filterKey = getFilterField(col);
|
|
19
|
+
const val = filters[filterKey];
|
|
20
|
+
if (!val)
|
|
21
|
+
return;
|
|
22
|
+
switch (val.type) {
|
|
23
|
+
case 'multiSelect':
|
|
24
|
+
if (val.value.length > 0) {
|
|
25
|
+
rows = rows.filter((r) => val.value.includes(String(getCellValue(r, col))));
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
case 'text':
|
|
29
|
+
if (val.value.trim()) {
|
|
30
|
+
const lower = val.value.trim().toLowerCase();
|
|
31
|
+
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase().includes(lower));
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case 'people': {
|
|
35
|
+
const email = val.value.email.toLowerCase();
|
|
36
|
+
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase() === email);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case 'date': {
|
|
40
|
+
const dv = val.value;
|
|
41
|
+
rows = rows.filter((r) => {
|
|
42
|
+
const cellVal = getCellValue(r, col);
|
|
43
|
+
if (cellVal == null)
|
|
44
|
+
return false;
|
|
45
|
+
const cellDate = new Date(String(cellVal));
|
|
46
|
+
if (Number.isNaN(cellDate.getTime()))
|
|
47
|
+
return false;
|
|
48
|
+
const cellDateStr = cellDate.toISOString().split('T')[0];
|
|
49
|
+
if (dv.from && cellDateStr < dv.from)
|
|
50
|
+
return false;
|
|
51
|
+
if (dv.to && cellDateStr > dv.to)
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// --- Sorting ---
|
|
60
|
+
if (sortBy) {
|
|
61
|
+
const sortCol = columns.find((c) => c.columnId === sortBy);
|
|
62
|
+
const compare = sortCol?.compare;
|
|
63
|
+
const dir = sortDirection === 'asc' ? 1 : -1;
|
|
64
|
+
rows.sort((a, b) => {
|
|
65
|
+
if (compare)
|
|
66
|
+
return compare(a, b) * dir;
|
|
67
|
+
const av = sortCol
|
|
68
|
+
? getCellValue(a, sortCol)
|
|
69
|
+
: a[sortBy];
|
|
70
|
+
const bv = sortCol
|
|
71
|
+
? getCellValue(b, sortCol)
|
|
72
|
+
: b[sortBy];
|
|
73
|
+
if (av == null && bv == null)
|
|
74
|
+
return 0;
|
|
75
|
+
if (av == null)
|
|
76
|
+
return -1 * dir;
|
|
77
|
+
if (bv == null)
|
|
78
|
+
return 1 * dir;
|
|
79
|
+
if (sortCol?.type === 'date') {
|
|
80
|
+
const at = new Date(String(av)).getTime();
|
|
81
|
+
const bt = new Date(String(bv)).getTime();
|
|
82
|
+
const aN = Number.isNaN(at) ? 0 : at;
|
|
83
|
+
const bN = Number.isNaN(bt) ? 0 : bt;
|
|
84
|
+
return aN === bN ? 0 : aN > bN ? dir : -dir;
|
|
85
|
+
}
|
|
86
|
+
if (typeof av === 'number' && typeof bv === 'number')
|
|
87
|
+
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
88
|
+
const as = String(av).toLowerCase();
|
|
89
|
+
const bs = String(bv).toLowerCase();
|
|
90
|
+
return as === bs ? 0 : as > bs ? dir : -dir;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return rows;
|
|
94
|
+
}
|
|
@@ -12,6 +12,7 @@ export function getHeaderFilterConfig(col, input) {
|
|
|
12
12
|
const filterType = (filterable?.type ?? 'none');
|
|
13
13
|
const filterField = filterable?.filterField ?? col.columnId;
|
|
14
14
|
const sortable = col.sortable !== false;
|
|
15
|
+
const filterValue = input.filters[filterField];
|
|
15
16
|
const base = {
|
|
16
17
|
columnKey: col.columnId,
|
|
17
18
|
columnName: col.name,
|
|
@@ -23,19 +24,15 @@ export function getHeaderFilterConfig(col, input) {
|
|
|
23
24
|
if (filterType === 'text') {
|
|
24
25
|
return {
|
|
25
26
|
...base,
|
|
26
|
-
textValue:
|
|
27
|
-
onTextChange: input.
|
|
28
|
-
? (v) => input.onTextFilterChange(filterField, v)
|
|
29
|
-
: undefined,
|
|
27
|
+
textValue: filterValue?.type === 'text' ? filterValue.value : '',
|
|
28
|
+
onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
if (filterType === 'people') {
|
|
33
32
|
return {
|
|
34
33
|
...base,
|
|
35
|
-
selectedUser:
|
|
36
|
-
onUserChange: input.
|
|
37
|
-
? (u) => input.onPeopleFilterChange(filterField, u)
|
|
38
|
-
: undefined,
|
|
34
|
+
selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
|
|
35
|
+
onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
|
|
39
36
|
peopleSearch: input.peopleSearch,
|
|
40
37
|
};
|
|
41
38
|
}
|
|
@@ -44,17 +41,15 @@ export function getHeaderFilterConfig(col, input) {
|
|
|
44
41
|
...base,
|
|
45
42
|
options: input.filterOptions[filterField] ?? [],
|
|
46
43
|
isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
|
|
47
|
-
selectedValues:
|
|
48
|
-
onFilterChange: (values) => input.
|
|
44
|
+
selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
|
|
45
|
+
onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
|
|
49
46
|
};
|
|
50
47
|
}
|
|
51
48
|
if (filterType === 'date') {
|
|
52
49
|
return {
|
|
53
50
|
...base,
|
|
54
|
-
dateValue:
|
|
55
|
-
onDateChange: input.
|
|
56
|
-
? (v) => input.onDateFilterChange(filterField, v)
|
|
57
|
-
: undefined,
|
|
51
|
+
dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
|
|
52
|
+
onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
|
|
58
53
|
};
|
|
59
54
|
}
|
|
60
55
|
return base;
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from
|
|
|
9
9
|
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
|
|
10
10
|
export { parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, } from './valueParsers';
|
|
11
11
|
export { computeAggregations } from './aggregationUtils';
|
|
12
|
+
export { processClientSideData } from './clientSideData';
|
|
@@ -4,16 +4,14 @@ export function getFilterField(col) {
|
|
|
4
4
|
const f = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
|
|
5
5
|
return (f?.filterField ?? col.columnId);
|
|
6
6
|
}
|
|
7
|
-
/** Merge a single filter change into a full IFilters object. */
|
|
7
|
+
/** Merge a single filter change into a full IFilters object. Strips empty values automatically. */
|
|
8
8
|
export function mergeFilter(prev, key, value) {
|
|
9
9
|
const next = { ...prev };
|
|
10
10
|
const isEmpty = value === undefined ||
|
|
11
|
-
(
|
|
12
|
-
(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
!(value.from || value.to);
|
|
16
|
-
if (isEmpty || isEmptyDate) {
|
|
11
|
+
(value.type === 'text' && value.value.trim() === '') ||
|
|
12
|
+
(value.type === 'multiSelect' && value.value.length === 0) ||
|
|
13
|
+
(value.type === 'date' && !value.value.from && !value.value.to);
|
|
14
|
+
if (isEmpty) {
|
|
17
15
|
delete next[key];
|
|
18
16
|
}
|
|
19
17
|
else {
|
|
@@ -10,17 +10,13 @@ export interface OGridLayoutProps {
|
|
|
10
10
|
containerComponent?: React.ElementType;
|
|
11
11
|
/** Extra props for the root container (e.g. sx for MUI Box). */
|
|
12
12
|
containerProps?: Record<string, unknown>;
|
|
13
|
-
/** Gap between deprecated title and the bordered container in px (default: 8). */
|
|
14
|
-
gap?: number | string;
|
|
15
13
|
className?: string;
|
|
16
|
-
/** @deprecated Render title outside OGrid. Renders above the bordered container during transition. */
|
|
17
|
-
title?: React.ReactNode;
|
|
18
14
|
/** Custom toolbar content (left-aligned in toolbar strip). */
|
|
19
15
|
toolbar?: React.ReactNode;
|
|
20
|
-
/** @deprecated Use toolbarEnd instead. */
|
|
21
|
-
columnChooser?: React.ReactNode;
|
|
22
16
|
/** Built-in toolbar items rendered on the right side (column chooser, etc.). */
|
|
23
17
|
toolbarEnd?: React.ReactNode;
|
|
18
|
+
/** Secondary toolbar row below the primary toolbar (e.g. active filter chips). Full width. */
|
|
19
|
+
toolbarBelow?: React.ReactNode;
|
|
24
20
|
/** Grid content (DataGridTable). */
|
|
25
21
|
children: React.ReactNode;
|
|
26
22
|
/** Pagination controls (rendered in footer strip inside the bordered container). */
|
|
@@ -30,7 +26,6 @@ export interface OGridLayoutProps {
|
|
|
30
26
|
}
|
|
31
27
|
/**
|
|
32
28
|
* Renders OGrid layout as a unified bordered container:
|
|
33
|
-
* [deprecated title above]
|
|
34
29
|
* ┌────────────────────────────────────┐
|
|
35
30
|
* │ [toolbar strip] │
|
|
36
31
|
* ├────────────────────────────────────┤
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses inline styles for framework-agnostic rendering.
|
|
5
5
|
*/
|
|
6
6
|
import * as React from 'react';
|
|
7
|
-
import type { IColumnDefinition,
|
|
7
|
+
import type { IColumnDefinition, SideBarPanelId, IFilters, FilterValue } from '../types';
|
|
8
8
|
/** Describes a filterable column for the sidebar filters panel. */
|
|
9
9
|
export interface SideBarFilterColumn {
|
|
10
10
|
columnId: string;
|
|
@@ -23,12 +23,8 @@ export interface SideBarProps {
|
|
|
23
23
|
/** Batch-set all visible columns at once (used by Select All / Clear All). */
|
|
24
24
|
onSetVisibleColumns: (columns: Set<string>) => void;
|
|
25
25
|
filterableColumns: SideBarFilterColumn[];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
onMultiSelectFilterChange: (key: string, values: string[]) => void;
|
|
29
|
-
onTextFilterChange: (key: string, value: string) => void;
|
|
30
|
-
dateFilters: Record<string, IDateFilterValue>;
|
|
31
|
-
onDateFilterChange: (key: string, value: IDateFilterValue | undefined) => void;
|
|
26
|
+
filters: IFilters;
|
|
27
|
+
onFilterChange: (key: string, value: FilterValue | undefined) => void;
|
|
32
28
|
filterOptions: Record<string, string[]>;
|
|
33
29
|
}
|
|
34
30
|
export declare function SideBar(props: SideBarProps): React.ReactElement;
|
|
@@ -22,7 +22,7 @@ export { useDebounce } from './useDebounce';
|
|
|
22
22
|
export { useFillHandle } from './useFillHandle';
|
|
23
23
|
export type { UseFillHandleResult, UseFillHandleParams } from './useFillHandle';
|
|
24
24
|
export { useDataGridState } from './useDataGridState';
|
|
25
|
-
export type { UseDataGridStateParams, UseDataGridStateResult } from './useDataGridState';
|
|
25
|
+
export type { UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, } from './useDataGridState';
|
|
26
26
|
export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
|
|
27
27
|
export type { UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, } from './useColumnHeaderFilterState';
|
|
28
28
|
export { useColumnChooserState } from './useColumnChooserState';
|
|
@@ -5,7 +5,8 @@ export interface UseDataGridStateParams<T> {
|
|
|
5
5
|
props: IOGridDataGridProps<T>;
|
|
6
6
|
wrapperRef: RefObject<HTMLDivElement | null>;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/** Column layout, visibility, and sizing state. */
|
|
9
|
+
export interface DataGridLayoutState<T> {
|
|
9
10
|
flatColumns: IColumnDef<T>[];
|
|
10
11
|
visibleCols: IColumnDef<T>[];
|
|
11
12
|
visibleColumnCount: number;
|
|
@@ -13,12 +14,28 @@ export interface UseDataGridStateResult<T> {
|
|
|
13
14
|
colOffset: number;
|
|
14
15
|
hasCheckboxCol: boolean;
|
|
15
16
|
rowIndexByRowId: Map<RowId, number>;
|
|
17
|
+
containerWidth: number;
|
|
18
|
+
minTableWidth: number;
|
|
19
|
+
desiredTableWidth: number;
|
|
20
|
+
columnSizingOverrides: Record<string, {
|
|
21
|
+
widthPx: number;
|
|
22
|
+
}>;
|
|
23
|
+
setColumnSizingOverrides: React.Dispatch<React.SetStateAction<Record<string, {
|
|
24
|
+
widthPx: number;
|
|
25
|
+
}>>>;
|
|
26
|
+
onColumnResized?: (columnId: string, width: number) => void;
|
|
27
|
+
}
|
|
28
|
+
/** Row selection (checkboxes, single-row click). */
|
|
29
|
+
export interface DataGridRowSelectionState {
|
|
16
30
|
selectedRowIds: Set<RowId>;
|
|
17
31
|
updateSelection: (newSelectedIds: Set<RowId>) => void;
|
|
18
32
|
handleRowCheckboxChange: (rowId: RowId, checked: boolean, rowIndex: number, shiftKey: boolean) => void;
|
|
19
33
|
handleSelectAll: (checked: boolean) => void;
|
|
20
34
|
allSelected: boolean;
|
|
21
35
|
someSelected: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Cell editing, popover editor, and commit/cancel helpers. */
|
|
38
|
+
export interface DataGridEditingState<T> {
|
|
22
39
|
editingCell: {
|
|
23
40
|
rowId: RowId;
|
|
24
41
|
columnId: string;
|
|
@@ -29,6 +46,13 @@ export interface UseDataGridStateResult<T> {
|
|
|
29
46
|
} | null) => void;
|
|
30
47
|
pendingEditorValue: unknown;
|
|
31
48
|
setPendingEditorValue: (value: unknown) => void;
|
|
49
|
+
commitCellEdit: (item: T, columnId: string, oldValue: unknown, newValue: unknown, rowIndex: number, globalColIndex: number) => void;
|
|
50
|
+
cancelPopoverEdit: () => void;
|
|
51
|
+
popoverAnchorEl: HTMLElement | null;
|
|
52
|
+
setPopoverAnchorEl: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
53
|
+
}
|
|
54
|
+
/** Cell selection, active cell, keyboard, clipboard, fill handle, undo/redo. */
|
|
55
|
+
export interface DataGridCellInteractionState {
|
|
32
56
|
activeCell: {
|
|
33
57
|
rowIndex: number;
|
|
34
58
|
columnIndex: number;
|
|
@@ -43,27 +67,12 @@ export interface UseDataGridStateResult<T> {
|
|
|
43
67
|
endRow: number;
|
|
44
68
|
endCol: number;
|
|
45
69
|
} | null;
|
|
46
|
-
setSelectionRange: (range:
|
|
70
|
+
setSelectionRange: (range: DataGridCellInteractionState['selectionRange']) => void;
|
|
47
71
|
handleCellMouseDown: (e: React.MouseEvent, rowIndex: number, globalColIndex: number) => void;
|
|
48
72
|
handleSelectAllCells: () => void;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} | null;
|
|
53
|
-
setContextMenu: (pos: {
|
|
54
|
-
x: number;
|
|
55
|
-
y: number;
|
|
56
|
-
} | null) => void;
|
|
57
|
-
handleCellContextMenu: (e: {
|
|
58
|
-
clientX: number;
|
|
59
|
-
clientY: number;
|
|
60
|
-
preventDefault?: () => void;
|
|
61
|
-
}) => void;
|
|
62
|
-
closeContextMenu: () => void;
|
|
63
|
-
canUndo: boolean;
|
|
64
|
-
canRedo: boolean;
|
|
65
|
-
onUndo?: () => void;
|
|
66
|
-
onRedo?: () => void;
|
|
73
|
+
hasCellSelection: boolean;
|
|
74
|
+
handleGridKeyDown: (e: React.KeyboardEvent) => void;
|
|
75
|
+
handleFillHandleMouseDown: (e: React.MouseEvent) => void;
|
|
67
76
|
handleCopy: () => void;
|
|
68
77
|
handleCut: () => void;
|
|
69
78
|
handlePaste: () => Promise<void>;
|
|
@@ -79,29 +88,44 @@ export interface UseDataGridStateResult<T> {
|
|
|
79
88
|
endRow: number;
|
|
80
89
|
endCol: number;
|
|
81
90
|
} | null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
clearClipboardRanges: () => void;
|
|
92
|
+
canUndo: boolean;
|
|
93
|
+
canRedo: boolean;
|
|
94
|
+
onUndo?: () => void;
|
|
95
|
+
onRedo?: () => void;
|
|
96
|
+
}
|
|
97
|
+
/** Context menu position and handlers. */
|
|
98
|
+
export interface DataGridContextMenuState {
|
|
99
|
+
menuPosition: {
|
|
100
|
+
x: number;
|
|
101
|
+
y: number;
|
|
102
|
+
} | null;
|
|
103
|
+
setMenuPosition: (pos: {
|
|
104
|
+
x: number;
|
|
105
|
+
y: number;
|
|
106
|
+
} | null) => void;
|
|
107
|
+
handleCellContextMenu: (e: {
|
|
108
|
+
clientX: number;
|
|
109
|
+
clientY: number;
|
|
110
|
+
preventDefault?: () => void;
|
|
111
|
+
}) => void;
|
|
112
|
+
closeContextMenu: () => void;
|
|
113
|
+
}
|
|
114
|
+
/** View model inputs and derived display state. */
|
|
115
|
+
export interface DataGridViewModelState<T> {
|
|
95
116
|
headerFilterInput: HeaderFilterConfigInput;
|
|
96
117
|
cellDescriptorInput: CellRenderDescriptorInput<T>;
|
|
97
|
-
commitCellEdit: (item: T, columnId: string, oldValue: unknown, newValue: unknown, rowIndex: number, globalColIndex: number) => void;
|
|
98
|
-
cancelPopoverEdit: () => void;
|
|
99
|
-
popoverAnchorEl: HTMLElement | null;
|
|
100
|
-
setPopoverAnchorEl: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
101
|
-
clearClipboardRanges: () => void;
|
|
102
118
|
statusBarConfig: IStatusBarProps | null;
|
|
103
119
|
showEmptyInGrid: boolean;
|
|
104
|
-
|
|
120
|
+
}
|
|
121
|
+
/** Grouped result from useDataGridState. */
|
|
122
|
+
export interface UseDataGridStateResult<T> {
|
|
123
|
+
layout: DataGridLayoutState<T>;
|
|
124
|
+
rowSelection: DataGridRowSelectionState;
|
|
125
|
+
editing: DataGridEditingState<T>;
|
|
126
|
+
interaction: DataGridCellInteractionState;
|
|
127
|
+
contextMenu: DataGridContextMenuState;
|
|
128
|
+
viewModels: DataGridViewModelState<T>;
|
|
105
129
|
}
|
|
106
130
|
/**
|
|
107
131
|
* Single orchestration hook for DataGridTable. Takes grid props and wrapper ref,
|
|
@@ -15,8 +15,8 @@ export interface UseOGridResult<T> {
|
|
|
15
15
|
handleVisibilityChange: (columnKey: string, isVisible: boolean) => void;
|
|
16
16
|
/** Resolved placement of the column chooser. */
|
|
17
17
|
columnChooserPlacement: ColumnChooserPlacement;
|
|
18
|
-
title: React.ReactNode;
|
|
19
18
|
toolbar: React.ReactNode;
|
|
19
|
+
toolbarBelow: React.ReactNode;
|
|
20
20
|
className?: string;
|
|
21
21
|
entityLabelPlural: string;
|
|
22
22
|
emptyState?: {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, HeaderCell, HeaderRow, SideBarPanelId, ISideBarDef, IDateFilterValue, } from './types';
|
|
2
|
-
export { toUserLike,
|
|
1
|
+
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, RowId, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, HeaderCell, HeaderRow, SideBarPanelId, ISideBarDef, IDateFilterValue, } from './types';
|
|
2
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
3
3
|
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, } from './hooks';
|
|
4
|
-
export type { UseFilterOptionsResult, UseOGridResult, ColumnChooserPlacement, UseActiveCellResult, UseCellEditingResult, EditingCell, UseContextMenuResult, ContextMenuPosition, UseCellSelectionResult, UseCellSelectionParams, UseClipboardResult, UseClipboardParams, UseRowSelectionResult, UseRowSelectionParams, UseKeyboardNavigationResult, UseKeyboardNavigationParams, UseUndoRedoResult, UseUndoRedoParams, UseFillHandleResult, UseFillHandleParams, UseDataGridStateParams, UseDataGridStateResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, UseColumnResizeParams, UseColumnResizeResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, } from './hooks';
|
|
4
|
+
export type { UseFilterOptionsResult, UseOGridResult, ColumnChooserPlacement, UseActiveCellResult, UseCellEditingResult, EditingCell, UseContextMenuResult, ContextMenuPosition, UseCellSelectionResult, UseCellSelectionParams, UseClipboardResult, UseClipboardParams, UseRowSelectionResult, UseRowSelectionParams, UseKeyboardNavigationResult, UseKeyboardNavigationParams, UseUndoRedoResult, UseUndoRedoParams, UseFillHandleResult, UseFillHandleParams, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, UseColumnResizeParams, UseColumnResizeResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, } from './hooks';
|
|
5
5
|
export { OGridLayout } from './components/OGridLayout';
|
|
6
6
|
export type { OGridLayoutProps } from './components/OGridLayout';
|
|
7
7
|
export { StatusBar } from './components/StatusBar';
|
|
@@ -12,5 +12,5 @@ export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
|
12
12
|
export type { MarchingAntsOverlayProps } from './components/MarchingAntsOverlay';
|
|
13
13
|
export { SideBar } from './components/SideBar';
|
|
14
14
|
export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
|
|
15
|
-
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, } from './utils';
|
|
15
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, } from './utils';
|
|
16
16
|
export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, ParseValueResult, AggregationResult, } from './utils';
|
|
@@ -18,19 +18,24 @@ export type UserLikeInput = {
|
|
|
18
18
|
photo?: string;
|
|
19
19
|
};
|
|
20
20
|
export declare function toUserLike(u: UserLikeInput | undefined): UserLike | undefined;
|
|
21
|
-
/**
|
|
22
|
-
export type FilterValue =
|
|
23
|
-
|
|
21
|
+
/** Discriminated filter value. The `type` field identifies the filter kind. */
|
|
22
|
+
export type FilterValue = {
|
|
23
|
+
type: 'text';
|
|
24
|
+
value: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'multiSelect';
|
|
27
|
+
value: string[];
|
|
28
|
+
} | {
|
|
29
|
+
type: 'people';
|
|
30
|
+
value: UserLike;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'date';
|
|
33
|
+
value: IDateFilterValue;
|
|
34
|
+
};
|
|
35
|
+
/** Unified filter model: field id -> discriminated filter value. */
|
|
24
36
|
export interface IFilters {
|
|
25
37
|
[field: string]: FilterValue | undefined;
|
|
26
38
|
}
|
|
27
|
-
/** Split IFilters into DataGridTable's multiSelect, text, people, and date props. */
|
|
28
|
-
export declare function toDataGridFilterProps(filters: IFilters): {
|
|
29
|
-
multiSelectFilters: Record<string, string[]>;
|
|
30
|
-
textFilters: Record<string, string>;
|
|
31
|
-
peopleFilters: Record<string, UserLike | undefined>;
|
|
32
|
-
dateFilters: Record<string, IDateFilterValue>;
|
|
33
|
-
};
|
|
34
39
|
export interface IFetchParams {
|
|
35
40
|
page: number;
|
|
36
41
|
pageSize: number;
|
|
@@ -192,14 +197,14 @@ export interface IOGridProps<T> {
|
|
|
192
197
|
defaultSortBy?: string;
|
|
193
198
|
defaultSortDirection?: 'asc' | 'desc';
|
|
194
199
|
toolbar?: ReactNode;
|
|
200
|
+
/** Secondary toolbar row rendered below the primary toolbar (e.g. active filter chips). */
|
|
201
|
+
toolbarBelow?: ReactNode;
|
|
195
202
|
emptyState?: {
|
|
196
203
|
message?: ReactNode;
|
|
197
204
|
render?: () => ReactNode;
|
|
198
205
|
};
|
|
199
206
|
entityLabelPlural?: string;
|
|
200
207
|
className?: string;
|
|
201
|
-
/** @deprecated Render your title outside the OGrid component. Will be removed in next major. */
|
|
202
|
-
title?: ReactNode;
|
|
203
208
|
/** Where the column chooser renders.
|
|
204
209
|
* - `true` or `'toolbar'` (default): column chooser button in the toolbar strip.
|
|
205
210
|
* - `'sidebar'`: column chooser only available via the sidebar columns panel.
|
|
@@ -260,14 +265,10 @@ export interface IOGridDataGridProps<T> {
|
|
|
260
265
|
selectedRows?: Set<RowId>;
|
|
261
266
|
onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
|
|
262
267
|
statusBar?: IStatusBarProps;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
peopleFilters?: Record<string, UserLike | undefined>;
|
|
268
|
-
onPeopleFilterChange?: (key: string, user: UserLike | undefined) => void;
|
|
269
|
-
dateFilters?: Record<string, IDateFilterValue>;
|
|
270
|
-
onDateFilterChange?: (key: string, value: IDateFilterValue | undefined) => void;
|
|
268
|
+
/** Unified filter model (discriminated union values). */
|
|
269
|
+
filters: IFilters;
|
|
270
|
+
/** Single callback for all filter changes. Pass undefined to clear. */
|
|
271
|
+
onFilterChange: (key: string, value: FilterValue | undefined) => void;
|
|
271
272
|
filterOptions: Record<string, string[]>;
|
|
272
273
|
loadingFilterOptions: Record<string, boolean>;
|
|
273
274
|
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
|
|
2
2
|
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, } from './dataGridTypes';
|
|
3
|
-
export { toUserLike,
|
|
3
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IColumnDef, IFilters } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Apply client-side filtering and sorting to data.
|
|
4
|
+
* Extracted from useOGrid for testability and reuse.
|
|
5
|
+
*
|
|
6
|
+
* @param data - The full dataset to process
|
|
7
|
+
* @param columns - Column definitions (used for filtering and sorting)
|
|
8
|
+
* @param filters - Current filter state (discriminated FilterValue union)
|
|
9
|
+
* @param sortBy - Column ID to sort by (optional)
|
|
10
|
+
* @param sortDirection - Sort direction (optional)
|
|
11
|
+
* @returns Filtered and sorted array
|
|
12
|
+
*/
|
|
13
|
+
export declare function processClientSideData<T>(data: T[], columns: IColumnDef<T>[], filters: IFilters, sortBy?: string, sortDirection?: 'asc' | 'desc'): T[];
|
|
@@ -4,22 +4,16 @@
|
|
|
4
4
|
import type * as React from 'react';
|
|
5
5
|
import type { ColumnFilterType, ICellEditorProps, IDateFilterValue } from '../types/columnTypes';
|
|
6
6
|
import type { IColumnDef } from '../types/columnTypes';
|
|
7
|
-
import type { RowId, UserLike } from '../types/dataGridTypes';
|
|
7
|
+
import type { RowId, UserLike, IFilters, FilterValue } from '../types/dataGridTypes';
|
|
8
8
|
export interface HeaderFilterConfigInput {
|
|
9
9
|
sortBy?: string;
|
|
10
10
|
sortDirection: 'asc' | 'desc';
|
|
11
11
|
onColumnSort: (columnKey: string) => void;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
peopleFilters?: Record<string, UserLike | undefined>;
|
|
15
|
-
onPeopleFilterChange?: (key: string, user: UserLike | undefined) => void;
|
|
16
|
-
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
12
|
+
filters: IFilters;
|
|
13
|
+
onFilterChange: (key: string, value: FilterValue | undefined) => void;
|
|
17
14
|
filterOptions: Record<string, string[]>;
|
|
18
15
|
loadingFilterOptions: Record<string, boolean>;
|
|
19
|
-
|
|
20
|
-
onMultiSelectFilterChange: (key: string, values: string[]) => void;
|
|
21
|
-
dateFilters?: Record<string, IDateFilterValue>;
|
|
22
|
-
onDateFilterChange?: (key: string, value: IDateFilterValue | undefined) => void;
|
|
16
|
+
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
23
17
|
}
|
|
24
18
|
/** Props to pass to ColumnHeaderFilter. Matches IColumnHeaderFilterProps. */
|
|
25
19
|
export interface HeaderFilterConfig {
|
|
@@ -16,3 +16,4 @@ export { parseValue, numberParser, currencyParser, dateParser, emailParser, bool
|
|
|
16
16
|
export type { ParseValueResult } from './valueParsers';
|
|
17
17
|
export { computeAggregations } from './aggregationUtils';
|
|
18
18
|
export type { AggregationResult } from './aggregationUtils';
|
|
19
|
+
export { processClientSideData } from './clientSideData';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IColumnDef, IFilters, FilterValue } from '../types';
|
|
2
2
|
/** Resolve the filter field key for a column (filterField or columnId). */
|
|
3
3
|
export declare function getFilterField<T>(col: IColumnDef<T>): string;
|
|
4
|
-
/** Merge a single filter change into a full IFilters object. */
|
|
4
|
+
/** Merge a single filter change into a full IFilters object. Strips empty values automatically. */
|
|
5
5
|
export declare function mergeFilter(prev: IFilters, key: string, value: FilterValue | undefined): IFilters;
|
|
6
6
|
/** Derive filter options for multiSelect columns from client-side data. */
|
|
7
7
|
export declare function deriveFilterOptionsFromData<T>(items: T[], columns: IColumnDef<T>[]): Record<string, string[]>;
|
package/package.json
CHANGED