@alaarab/ogrid-core 1.4.0 → 1.6.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/OGridLayout.js +3 -7
- package/dist/esm/components/SideBar.js +14 -12
- package/dist/esm/hooks/useActiveCell.js +5 -0
- package/dist/esm/hooks/useColumnResize.js +54 -40
- package/dist/esm/hooks/useDataGridState.js +90 -82
- package/dist/esm/hooks/useOGrid.js +43 -117
- 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/aggregationUtils.js +3 -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/esm/utils/statusBarHelpers.js +4 -2
- package/dist/types/components/OGridLayout.d.ts +0 -7
- package/dist/types/components/SideBar.d.ts +3 -7
- package/dist/types/components/StatusBar.d.ts +1 -0
- 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 +0 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/types/dataGridTypes.d.ts +31 -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/dist/types/utils/statusBarHelpers.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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, 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, 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'
|
|
@@ -36,6 +35,7 @@ export function useOGrid(props, ref) {
|
|
|
36
35
|
return new Set(visible.length > 0 ? visible : columns.map((c) => c.columnId));
|
|
37
36
|
});
|
|
38
37
|
const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
|
|
38
|
+
const [pinnedOverrides, setPinnedOverrides] = useState({});
|
|
39
39
|
const page = controlledPage ?? internalPage;
|
|
40
40
|
const pageSize = controlledPageSize ?? internalPageSize;
|
|
41
41
|
const sort = controlledSort ?? internalSort;
|
|
@@ -69,23 +69,14 @@ export function useOGrid(props, ref) {
|
|
|
69
69
|
setInternalVisibleColumns(cols);
|
|
70
70
|
onVisibleColumnsChange?.(cols);
|
|
71
71
|
}, [controlledVisibleColumns, onVisibleColumnsChange]);
|
|
72
|
-
const { multiSelectFilters, textFilters, peopleFilters, dateFilters } = useMemo(() => toDataGridFilterProps(filters), [filters]);
|
|
73
72
|
const handleSort = useCallback((columnKey) => {
|
|
74
73
|
setSort({
|
|
75
74
|
field: columnKey,
|
|
76
75
|
direction: sort.field === columnKey && sort.direction === 'asc' ? 'desc' : 'asc',
|
|
77
76
|
});
|
|
78
77
|
}, [sort, setSort]);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}, [filters, setFilters]);
|
|
82
|
-
const handleTextFilterChange = useCallback((key, value) => {
|
|
83
|
-
setFilters(mergeFilter(filters, key, value.trim() || undefined));
|
|
84
|
-
}, [filters, setFilters]);
|
|
85
|
-
const handlePeopleFilterChange = useCallback((key, user) => {
|
|
86
|
-
setFilters(mergeFilter(filters, key, user ?? undefined));
|
|
87
|
-
}, [filters, setFilters]);
|
|
88
|
-
const handleDateFilterChange = useCallback((key, value) => {
|
|
78
|
+
/** Single filter change handler — wraps discriminated FilterValue into mergeFilter. */
|
|
79
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
89
80
|
setFilters(mergeFilter(filters, key, value));
|
|
90
81
|
}, [filters, setFilters]);
|
|
91
82
|
const handleVisibilityChange = useCallback((columnKey, isVisible) => {
|
|
@@ -112,88 +103,11 @@ export function useOGrid(props, ref) {
|
|
|
112
103
|
return serverFilterOptions;
|
|
113
104
|
return deriveFilterOptionsFromData(displayData, columns);
|
|
114
105
|
}, [dataSource, displayData, columns, serverFilterOptions]);
|
|
106
|
+
// --- Client-side filtering & sorting ---
|
|
115
107
|
const clientItemsAndTotal = useMemo(() => {
|
|
116
108
|
if (!isClientSide)
|
|
117
109
|
return null;
|
|
118
|
-
|
|
119
|
-
columns.forEach((col) => {
|
|
120
|
-
const filterKey = getFilterField(col);
|
|
121
|
-
const f = col.filterable && typeof col.filterable === 'object'
|
|
122
|
-
? col.filterable
|
|
123
|
-
: null;
|
|
124
|
-
const type = f?.type;
|
|
125
|
-
const val = filters[filterKey];
|
|
126
|
-
if (type === 'multiSelect' && Array.isArray(val) && val.length > 0) {
|
|
127
|
-
rows = rows.filter((r) => val.includes(String(getCellValue(r, col))));
|
|
128
|
-
}
|
|
129
|
-
else if (type === 'text' &&
|
|
130
|
-
typeof val === 'string' &&
|
|
131
|
-
val.trim()) {
|
|
132
|
-
const lower = val.trim().toLowerCase();
|
|
133
|
-
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase().includes(lower));
|
|
134
|
-
}
|
|
135
|
-
else if (type === 'people' &&
|
|
136
|
-
val &&
|
|
137
|
-
typeof val === 'object' &&
|
|
138
|
-
'email' in val) {
|
|
139
|
-
const email = val.email.toLowerCase();
|
|
140
|
-
rows = rows.filter((r) => String(getCellValue(r, col) ?? '').toLowerCase() === email);
|
|
141
|
-
}
|
|
142
|
-
else if (type === 'date' &&
|
|
143
|
-
val &&
|
|
144
|
-
typeof val === 'object' &&
|
|
145
|
-
!Array.isArray(val) &&
|
|
146
|
-
('from' in val || 'to' in val)) {
|
|
147
|
-
const dv = val;
|
|
148
|
-
rows = rows.filter((r) => {
|
|
149
|
-
const cellVal = getCellValue(r, col);
|
|
150
|
-
if (cellVal == null)
|
|
151
|
-
return false;
|
|
152
|
-
const cellDate = new Date(String(cellVal));
|
|
153
|
-
if (Number.isNaN(cellDate.getTime()))
|
|
154
|
-
return false;
|
|
155
|
-
const cellDateStr = cellDate.toISOString().split('T')[0];
|
|
156
|
-
if (dv.from && cellDateStr < dv.from)
|
|
157
|
-
return false;
|
|
158
|
-
if (dv.to && cellDateStr > dv.to)
|
|
159
|
-
return false;
|
|
160
|
-
return true;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
if (sort.field) {
|
|
165
|
-
const sortCol = columns.find((c) => c.columnId === sort.field);
|
|
166
|
-
const compare = sortCol?.compare;
|
|
167
|
-
const dir = sort.direction === 'asc' ? 1 : -1;
|
|
168
|
-
rows.sort((a, b) => {
|
|
169
|
-
if (compare)
|
|
170
|
-
return compare(a, b) * dir;
|
|
171
|
-
const av = sortCol
|
|
172
|
-
? getCellValue(a, sortCol)
|
|
173
|
-
: a[sort.field];
|
|
174
|
-
const bv = sortCol
|
|
175
|
-
? getCellValue(b, sortCol)
|
|
176
|
-
: b[sort.field];
|
|
177
|
-
if (av == null && bv == null)
|
|
178
|
-
return 0;
|
|
179
|
-
if (av == null)
|
|
180
|
-
return -1 * dir;
|
|
181
|
-
if (bv == null)
|
|
182
|
-
return 1 * dir;
|
|
183
|
-
if (sortCol?.type === 'date') {
|
|
184
|
-
const at = new Date(String(av)).getTime();
|
|
185
|
-
const bt = new Date(String(bv)).getTime();
|
|
186
|
-
const aN = Number.isNaN(at) ? 0 : at;
|
|
187
|
-
const bN = Number.isNaN(bt) ? 0 : bt;
|
|
188
|
-
return aN === bN ? 0 : aN > bN ? dir : -dir;
|
|
189
|
-
}
|
|
190
|
-
if (typeof av === 'number' && typeof bv === 'number')
|
|
191
|
-
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
192
|
-
const as = String(av).toLowerCase();
|
|
193
|
-
const bs = String(bv).toLowerCase();
|
|
194
|
-
return as === bs ? 0 : as > bs ? dir : -dir;
|
|
195
|
-
});
|
|
196
|
-
}
|
|
110
|
+
const rows = processClientSideData(displayData, columns, filters, sort.field, sort.direction);
|
|
197
111
|
const total = rows.length;
|
|
198
112
|
const start = (page - 1) * pageSize;
|
|
199
113
|
const paged = rows.slice(start, start + pageSize);
|
|
@@ -260,6 +174,14 @@ export function useOGrid(props, ref) {
|
|
|
260
174
|
const displayTotalCount = isClientSide && clientItemsAndTotal
|
|
261
175
|
? clientItemsAndTotal.totalCount
|
|
262
176
|
: serverTotalCount;
|
|
177
|
+
// Fire onFirstDataRendered once when the grid first has data
|
|
178
|
+
const firstDataRenderedRef = useRef(false);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!firstDataRenderedRef.current && displayItems.length > 0) {
|
|
181
|
+
firstDataRenderedRef.current = true;
|
|
182
|
+
onFirstDataRendered?.();
|
|
183
|
+
}
|
|
184
|
+
}, [displayItems.length, onFirstDataRendered]);
|
|
263
185
|
useImperativeHandle(ref, () => ({
|
|
264
186
|
setRowData: (d) => {
|
|
265
187
|
if (!isServerSide)
|
|
@@ -272,6 +194,7 @@ export function useOGrid(props, ref) {
|
|
|
272
194
|
columnOrder: columnOrder ?? undefined,
|
|
273
195
|
columnWidths: Object.keys(columnWidthOverrides).length > 0 ? columnWidthOverrides : undefined,
|
|
274
196
|
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
197
|
+
pinnedColumns: Object.keys(pinnedOverrides).length > 0 ? pinnedOverrides : undefined,
|
|
275
198
|
}),
|
|
276
199
|
applyColumnState: (state) => {
|
|
277
200
|
if (state.visibleColumns) {
|
|
@@ -289,6 +212,9 @@ export function useOGrid(props, ref) {
|
|
|
289
212
|
if (state.filters) {
|
|
290
213
|
setFilters(state.filters);
|
|
291
214
|
}
|
|
215
|
+
if (state.pinnedColumns) {
|
|
216
|
+
setPinnedOverrides(state.pinnedColumns);
|
|
217
|
+
}
|
|
292
218
|
},
|
|
293
219
|
setFilterModel: setFilters,
|
|
294
220
|
getSelectedRows: () => Array.from(effectiveSelectedRows),
|
|
@@ -318,6 +244,7 @@ export function useOGrid(props, ref) {
|
|
|
318
244
|
sort,
|
|
319
245
|
columnOrder,
|
|
320
246
|
columnWidthOverrides,
|
|
247
|
+
pinnedOverrides,
|
|
321
248
|
filters,
|
|
322
249
|
setFilters,
|
|
323
250
|
setSort,
|
|
@@ -330,9 +257,9 @@ export function useOGrid(props, ref) {
|
|
|
330
257
|
getRowId,
|
|
331
258
|
onSelectionChange,
|
|
332
259
|
]);
|
|
260
|
+
// With discriminated union, any defined value is active (mergeFilter already strips empties)
|
|
333
261
|
const hasActiveFilters = useMemo(() => {
|
|
334
|
-
return Object.values(filters).some((v) => v !== undefined
|
|
335
|
-
(Array.isArray(v) ? v.length > 0 : typeof v === 'string' ? v.trim() !== '' : true));
|
|
262
|
+
return Object.values(filters).some((v) => v !== undefined);
|
|
336
263
|
}, [filters]);
|
|
337
264
|
const columnChooserColumns = useMemo(() => columns.map((c) => ({
|
|
338
265
|
columnId: c.columnId,
|
|
@@ -350,6 +277,7 @@ export function useOGrid(props, ref) {
|
|
|
350
277
|
totalCount: totalData,
|
|
351
278
|
filteredCount: hasActiveFilters ? filteredData : undefined,
|
|
352
279
|
selectedCount: effectiveSelectedRows.size,
|
|
280
|
+
suppressRowCount: true, // OGrid always has pagination which shows the total
|
|
353
281
|
};
|
|
354
282
|
}, [
|
|
355
283
|
statusBar,
|
|
@@ -364,6 +292,17 @@ export function useOGrid(props, ref) {
|
|
|
364
292
|
setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
|
|
365
293
|
onColumnResized?.(columnId, width);
|
|
366
294
|
}, [onColumnResized]);
|
|
295
|
+
const handleColumnPinned = useCallback((columnId, pinned) => {
|
|
296
|
+
setPinnedOverrides((prev) => {
|
|
297
|
+
if (pinned === null) {
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
299
|
+
const { [columnId]: _, ...rest } = prev;
|
|
300
|
+
return rest;
|
|
301
|
+
}
|
|
302
|
+
return { ...prev, [columnId]: pinned };
|
|
303
|
+
});
|
|
304
|
+
onColumnPinned?.(columnId, pinned);
|
|
305
|
+
}, [onColumnPinned]);
|
|
367
306
|
// --- Side bar ---
|
|
368
307
|
const sideBarState = useSideBarState({ config: sideBar });
|
|
369
308
|
const filterableColumns = useMemo(() => columns
|
|
@@ -387,12 +326,8 @@ export function useOGrid(props, ref) {
|
|
|
387
326
|
onVisibilityChange: handleVisibilityChange,
|
|
388
327
|
onSetVisibleColumns: setVisibleColumns,
|
|
389
328
|
filterableColumns,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
onMultiSelectFilterChange: handleMultiSelectFilterChange,
|
|
393
|
-
onTextFilterChange: handleTextFilterChange,
|
|
394
|
-
dateFilters,
|
|
395
|
-
onDateFilterChange: handleDateFilterChange,
|
|
329
|
+
filters,
|
|
330
|
+
onFilterChange: handleFilterChange,
|
|
396
331
|
filterOptions: clientFilterOptions,
|
|
397
332
|
};
|
|
398
333
|
}, [
|
|
@@ -406,12 +341,8 @@ export function useOGrid(props, ref) {
|
|
|
406
341
|
handleVisibilityChange,
|
|
407
342
|
setVisibleColumns,
|
|
408
343
|
filterableColumns,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
handleMultiSelectFilterChange,
|
|
412
|
-
handleTextFilterChange,
|
|
413
|
-
dateFilters,
|
|
414
|
-
handleDateFilterChange,
|
|
344
|
+
filters,
|
|
345
|
+
handleFilterChange,
|
|
415
346
|
clientFilterOptions,
|
|
416
347
|
]);
|
|
417
348
|
const dataGridProps = {
|
|
@@ -425,6 +356,8 @@ export function useOGrid(props, ref) {
|
|
|
425
356
|
columnOrder,
|
|
426
357
|
onColumnOrderChange,
|
|
427
358
|
onColumnResized: handleColumnResized,
|
|
359
|
+
onColumnPinned: handleColumnPinned,
|
|
360
|
+
pinnedColumns: pinnedOverrides,
|
|
428
361
|
initialColumnWidths: columnWidthOverrides,
|
|
429
362
|
freezeRows,
|
|
430
363
|
freezeCols,
|
|
@@ -440,14 +373,8 @@ export function useOGrid(props, ref) {
|
|
|
440
373
|
onSelectionChange: handleSelectionChange,
|
|
441
374
|
statusBar: statusBarConfig,
|
|
442
375
|
isLoading: (isServerSide && loading) || displayLoading,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
textFilters,
|
|
446
|
-
onTextFilterChange: handleTextFilterChange,
|
|
447
|
-
peopleFilters,
|
|
448
|
-
onPeopleFilterChange: handlePeopleFilterChange,
|
|
449
|
-
dateFilters,
|
|
450
|
-
onDateFilterChange: handleDateFilterChange,
|
|
376
|
+
filters,
|
|
377
|
+
onFilterChange: handleFilterChange,
|
|
451
378
|
filterOptions: clientFilterOptions,
|
|
452
379
|
loadingFilterOptions: dataSource?.fetchFilterOptions ? loadingFilterOptions : {},
|
|
453
380
|
peopleSearch: dataSource?.searchPeople,
|
|
@@ -474,7 +401,6 @@ export function useOGrid(props, ref) {
|
|
|
474
401
|
visibleColumns,
|
|
475
402
|
handleVisibilityChange,
|
|
476
403
|
columnChooserPlacement,
|
|
477
|
-
title,
|
|
478
404
|
toolbar,
|
|
479
405
|
className,
|
|
480
406
|
entityLabelPlural,
|
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';
|
|
@@ -19,7 +19,9 @@ export function computeAggregations(items, visibleCols, selectionRange) {
|
|
|
19
19
|
const item = items[r];
|
|
20
20
|
const col = visibleCols[c];
|
|
21
21
|
const raw = getCellValue(item, col);
|
|
22
|
-
|
|
22
|
+
// Use Number() instead of parseFloat() so date strings like "2020-08-22"
|
|
23
|
+
// return NaN instead of partially parsing to 2020
|
|
24
|
+
const num = typeof raw === 'number' ? raw : Number(raw);
|
|
23
25
|
if (!isNaN(num) && isFinite(num)) {
|
|
24
26
|
numericValues.push(num);
|
|
25
27
|
}
|
|
@@ -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 {
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Returns an array of status bar parts (Rows, Filtered, Selected) for consistent rendering across packages.
|
|
3
3
|
*/
|
|
4
4
|
export function getStatusBarParts(input) {
|
|
5
|
-
const { totalCount, filteredCount, selectedCount, selectedCellCount, aggregation } = input;
|
|
5
|
+
const { totalCount, filteredCount, selectedCount, selectedCellCount, aggregation, suppressRowCount } = input;
|
|
6
6
|
const parts = [];
|
|
7
|
-
|
|
7
|
+
if (!suppressRowCount) {
|
|
8
|
+
parts.push({ key: 'total', label: 'Rows:', value: totalCount });
|
|
9
|
+
}
|
|
8
10
|
if (filteredCount !== undefined && filteredCount !== totalCount) {
|
|
9
11
|
parts.push({ key: 'filtered', label: 'Filtered:', value: filteredCount });
|
|
10
12
|
}
|
|
@@ -10,15 +10,9 @@ 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;
|
|
24
18
|
/** Grid content (DataGridTable). */
|
|
@@ -30,7 +24,6 @@ export interface OGridLayoutProps {
|
|
|
30
24
|
}
|
|
31
25
|
/**
|
|
32
26
|
* Renders OGrid layout as a unified bordered container:
|
|
33
|
-
* [deprecated title above]
|
|
34
27
|
* ┌────────────────────────────────────┐
|
|
35
28
|
* │ [toolbar strip] │
|
|
36
29
|
* ├────────────────────────────────────┤
|
|
@@ -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';
|