@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.
@@ -1,12 +1,11 @@
1
1
  import { useMemo, useCallback, useState, useEffect, useRef, useImperativeHandle, } from 'react';
2
- import { getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from '../utils/ogridHelpers';
3
- import { getCellValue, flattenColumns } from '../utils';
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, title, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, pageSizeOptions, sideBar, onError, columnChooser: columnChooserProp, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
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
- const handleMultiSelectFilterChange = useCallback((key, values) => {
80
- setFilters(mergeFilter(filters, key, values.length ? values : undefined));
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
- let rows = displayData.slice();
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
- multiSelectFilters,
391
- textFilters: textFilters ?? {},
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
- multiSelectFilters,
410
- textFilters,
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
- multiSelectFilters,
444
- onMultiSelectFilterChange: handleMultiSelectFilterChange,
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, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './types';
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);
@@ -1 +1 @@
1
- export { toUserLike, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
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
- const num = typeof raw === 'number' ? raw : parseFloat(String(raw));
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: input.textFilters?.[filterField] ?? '',
27
- onTextChange: input.onTextFilterChange
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: input.peopleFilters?.[filterField],
36
- onUserChange: input.onPeopleFilterChange
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: input.multiSelectFilters[filterField] ?? [],
48
- onFilterChange: (values) => input.onMultiSelectFilterChange(filterField, values),
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: input.dateFilters?.[filterField],
55
- onDateChange: input.onDateFilterChange
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;
@@ -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
- (Array.isArray(value) && value.length === 0) ||
12
- (typeof value === 'string' && value.trim() === '');
13
- // Date filter is empty when neither from nor to is set
14
- const isEmptyDate = typeof value === 'object' && value !== null && !Array.isArray(value) && !('email' in value) &&
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
- parts.push({ key: 'total', label: 'Rows:', value: totalCount });
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, IDateFilterValue, SideBarPanelId } from '../types';
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
- multiSelectFilters: Record<string, string[]>;
27
- textFilters: Record<string, string>;
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;
@@ -18,6 +18,7 @@ export interface StatusBarProps {
18
18
  max: number;
19
19
  count: number;
20
20
  } | null;
21
+ suppressRowCount?: boolean;
21
22
  classNames?: StatusBarClassNames;
22
23
  }
23
24
  export declare function StatusBar({ classNames, ...rest }: StatusBarProps): 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';