@alaarab/ogrid-core 1.5.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.
@@ -56,7 +56,6 @@ const gridChildStyle = {
56
56
  };
57
57
  /**
58
58
  * Renders OGrid layout as a unified bordered container:
59
- * [deprecated title above]
60
59
  * ┌────────────────────────────────────┐
61
60
  * │ [toolbar strip] │
62
61
  * ├────────────────────────────────────┤
@@ -66,18 +65,14 @@ const gridChildStyle = {
66
65
  * └────────────────────────────────────┘
67
66
  */
68
67
  export function OGridLayout(props) {
69
- const { containerComponent: Container = 'div', containerProps = {}, gap = 8, className, title, toolbar, columnChooser, toolbarEnd: toolbarEndProp, children, pagination, sideBar, } = props;
68
+ const { containerComponent: Container = 'div', containerProps = {}, className, toolbar, toolbarEnd, children, pagination, sideBar, } = props;
70
69
  const hasSideBar = sideBar != null;
71
70
  const sideBarPosition = sideBar?.position ?? 'right';
72
- // Backward compat: columnChooser prop → toolbarEnd
73
- const toolbarEnd = toolbarEndProp ?? columnChooser;
74
71
  const hasToolbar = toolbar != null || toolbarEnd != null;
75
- // Root styles: flex column, fill parent height, gap for deprecated title spacing
76
72
  const rootStyle = {
77
73
  display: 'flex',
78
74
  flexDirection: 'column',
79
75
  height: '100%',
80
- gap: title != null ? (typeof gap === 'number' ? `${gap}px` : gap) : undefined,
81
76
  };
82
- return (_jsxs(Container, { className: className, style: rootStyle, ...containerProps, children: [title != null && _jsx("div", { style: { margin: 0 }, children: title }), _jsxs("div", { style: borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsx("div", { style: toolbarSectionStyle, children: toolbarEnd })] })), 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 }))] })] }));
77
+ 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 })] })), 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
78
  }
@@ -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, multiSelectFilters, textFilters, onMultiSelectFilterChange, onTextFilterChange, dateFilters, onDateFilterChange, filterOptions, } = props;
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, multiSelectFilters: multiSelectFilters, textFilters: textFilters, onMultiSelectFilterChange: onMultiSelectFilterChange, onTextFilterChange: onTextFilterChange, dateFilters: dateFilters, onDateFilterChange: onDateFilterChange, filterOptions: filterOptions }))] })] })) : null;
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, multiSelectFilters, textFilters, onMultiSelectFilterChange, onTextFilterChange, dateFilters, onDateFilterChange, filterOptions } = props;
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: textFilters[filterKey] ?? '', onChange: (e) => onTextFilterChange(filterKey, e.target.value), 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: dateFilters[filterKey]?.from ?? '', onChange: (e) => {
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 to = dateFilters[filterKey]?.to;
82
- onDateFilterChange(filterKey, from || to ? { from, to } : undefined);
83
- }, "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: dateFilters[filterKey]?.to ?? '', onChange: (e) => {
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 from = dateFilters[filterKey]?.from;
86
- onDateFilterChange(filterKey, from || to ? { from, to } : undefined);
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 = (multiSelectFilters[filterKey] ?? []).includes(opt);
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 = multiSelectFilters[filterKey] ?? [];
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
- onMultiSelectFilterChange(filterKey, next);
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
- const startWidth = columnSizingOverrides[columnId]?.widthPx
12
- ?? col.idealWidth
13
- ?? col.defaultWidth
14
- ?? defaultWidth;
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;
@@ -209,37 +209,25 @@ export function useDataGridState(params) {
209
209
  const showEmptyInGrid = items.length === 0 && !!emptyState;
210
210
  const hasCellSelection = selectionRange != null || activeCell != null;
211
211
  // --- View-model inputs (shared across all 3 DataGridTables) ---
212
- const { sortBy, sortDirection, onColumnSort, textFilters = {}, onTextFilterChange, peopleFilters = {}, onPeopleFilterChange, peopleSearch, filterOptions, loadingFilterOptions, multiSelectFilters, onMultiSelectFilterChange, dateFilters = {}, onDateFilterChange, } = props;
212
+ const { sortBy, sortDirection, onColumnSort, filters, onFilterChange, filterOptions, loadingFilterOptions, peopleSearch, } = props;
213
213
  const headerFilterInput = useMemo(() => ({
214
214
  sortBy,
215
215
  sortDirection,
216
216
  onColumnSort,
217
- textFilters,
218
- onTextFilterChange,
219
- peopleFilters,
220
- onPeopleFilterChange,
221
- peopleSearch,
217
+ filters,
218
+ onFilterChange,
222
219
  filterOptions,
223
220
  loadingFilterOptions,
224
- multiSelectFilters,
225
- onMultiSelectFilterChange,
226
- dateFilters,
227
- onDateFilterChange,
221
+ peopleSearch,
228
222
  }), [
229
223
  sortBy,
230
224
  sortDirection,
231
225
  onColumnSort,
232
- textFilters,
233
- onTextFilterChange,
234
- peopleFilters,
235
- onPeopleFilterChange,
236
- peopleSearch,
226
+ filters,
227
+ onFilterChange,
237
228
  filterOptions,
238
229
  loadingFilterOptions,
239
- multiSelectFilters,
240
- onMultiSelectFilterChange,
241
- dateFilters,
242
- onDateFilterChange,
230
+ peopleSearch,
243
231
  ]);
244
232
  const cellDescriptorInput = useMemo(() => ({
245
233
  editingCell,
@@ -305,59 +293,71 @@ export function useDataGridState(params) {
305
293
  setPendingEditorValue(undefined);
306
294
  }, [setEditingCell, setPendingEditorValue]);
307
295
  return {
308
- flatColumns,
309
- visibleCols,
310
- visibleColumnCount,
311
- totalColCount,
312
- colOffset,
313
- hasCheckboxCol,
314
- rowIndexByRowId,
315
- selectedRowIds,
316
- updateSelection,
317
- handleRowCheckboxChange,
318
- handleSelectAll,
319
- allSelected,
320
- someSelected,
321
- editingCell,
322
- setEditingCell,
323
- pendingEditorValue,
324
- setPendingEditorValue,
325
- activeCell: cellSelection ? activeCell : null,
326
- setActiveCell: cellSelection ? setActiveCell : NOOP,
327
- selectionRange: cellSelection ? selectionRange : null,
328
- setSelectionRange: cellSelection ? setSelectionRange : NOOP,
329
- handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
330
- handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
331
- contextMenu: cellSelection ? contextMenu : null,
332
- setContextMenu: cellSelection ? setContextMenu : NOOP,
333
- handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
334
- closeContextMenu: cellSelection ? closeContextMenu : NOOP,
335
- canUndo: undoRedo.canUndo,
336
- canRedo: undoRedo.canRedo,
337
- onUndo: undoRedo.undo,
338
- onRedo: undoRedo.redo,
339
- handleCopy: cellSelection ? handleCopy : NOOP,
340
- handleCut: cellSelection ? handleCut : NOOP,
341
- handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
342
- cutRange: cellSelection ? cutRange : null,
343
- copyRange: cellSelection ? copyRange : null,
344
- handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
345
- handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
346
- containerWidth,
347
- minTableWidth,
348
- desiredTableWidth,
349
- columnSizingOverrides,
350
- setColumnSizingOverrides,
351
- onColumnResized,
352
- headerFilterInput,
353
- cellDescriptorInput,
354
- commitCellEdit,
355
- cancelPopoverEdit,
356
- popoverAnchorEl,
357
- setPopoverAnchorEl,
358
- clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
359
- statusBarConfig,
360
- showEmptyInGrid,
361
- hasCellSelection: cellSelection ? hasCellSelection : false,
296
+ layout: {
297
+ flatColumns,
298
+ visibleCols,
299
+ visibleColumnCount,
300
+ totalColCount,
301
+ colOffset,
302
+ hasCheckboxCol,
303
+ rowIndexByRowId,
304
+ containerWidth,
305
+ minTableWidth,
306
+ desiredTableWidth,
307
+ columnSizingOverrides,
308
+ setColumnSizingOverrides,
309
+ onColumnResized,
310
+ },
311
+ rowSelection: {
312
+ selectedRowIds,
313
+ updateSelection,
314
+ handleRowCheckboxChange,
315
+ handleSelectAll,
316
+ allSelected,
317
+ someSelected,
318
+ },
319
+ editing: {
320
+ editingCell,
321
+ setEditingCell,
322
+ pendingEditorValue,
323
+ setPendingEditorValue,
324
+ commitCellEdit,
325
+ cancelPopoverEdit,
326
+ popoverAnchorEl,
327
+ setPopoverAnchorEl,
328
+ },
329
+ interaction: {
330
+ activeCell: cellSelection ? activeCell : null,
331
+ setActiveCell: cellSelection ? setActiveCell : NOOP,
332
+ selectionRange: cellSelection ? selectionRange : null,
333
+ setSelectionRange: cellSelection ? setSelectionRange : NOOP,
334
+ handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
335
+ handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
336
+ hasCellSelection: cellSelection ? hasCellSelection : false,
337
+ handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
338
+ handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
339
+ handleCopy: cellSelection ? handleCopy : NOOP,
340
+ handleCut: cellSelection ? handleCut : NOOP,
341
+ handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
342
+ cutRange: cellSelection ? cutRange : null,
343
+ copyRange: cellSelection ? copyRange : null,
344
+ clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
345
+ canUndo: undoRedo.canUndo,
346
+ canRedo: undoRedo.canRedo,
347
+ onUndo: undoRedo.undo,
348
+ onRedo: undoRedo.redo,
349
+ },
350
+ contextMenu: {
351
+ menuPosition: cellSelection ? contextMenu : null,
352
+ setMenuPosition: cellSelection ? setContextMenu : NOOP,
353
+ handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
354
+ closeContextMenu: cellSelection ? closeContextMenu : NOOP,
355
+ },
356
+ viewModels: {
357
+ headerFilterInput,
358
+ cellDescriptorInput,
359
+ statusBarConfig,
360
+ showEmptyInGrid,
361
+ },
362
362
  };
363
363
  }
@@ -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, onColumnPinned, 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, onFirstDataRendered, 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'
@@ -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
- const handleMultiSelectFilterChange = useCallback((key, values) => {
81
- setFilters(mergeFilter(filters, key, values.length ? values : undefined));
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
- let rows = displayData.slice();
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
- multiSelectFilters,
417
- textFilters: textFilters ?? {},
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
- multiSelectFilters,
436
- textFilters,
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
- multiSelectFilters,
472
- onMultiSelectFilterChange: handleMultiSelectFilterChange,
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,7 +401,6 @@ export function useOGrid(props, ref) {
502
401
  visibleColumns,
503
402
  handleVisibilityChange,
504
403
  columnChooserPlacement,
505
- title,
506
404
  toolbar,
507
405
  className,
508
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';
@@ -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 {
@@ -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;
@@ -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
- export interface UseDataGridStateResult<T> {
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: UseDataGridStateResult<T>['selectionRange']) => void;
70
+ setSelectionRange: (range: DataGridCellInteractionState['selectionRange']) => void;
47
71
  handleCellMouseDown: (e: React.MouseEvent, rowIndex: number, globalColIndex: number) => void;
48
72
  handleSelectAllCells: () => void;
49
- contextMenu: {
50
- x: number;
51
- y: number;
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
- handleGridKeyDown: (e: React.KeyboardEvent) => void;
83
- handleFillHandleMouseDown: (e: React.MouseEvent) => void;
84
- containerWidth: number;
85
- minTableWidth: number;
86
- desiredTableWidth: number;
87
- columnSizingOverrides: Record<string, {
88
- widthPx: number;
89
- }>;
90
- setColumnSizingOverrides: React.Dispatch<React.SetStateAction<Record<string, {
91
- widthPx: number;
92
- }>>>;
93
- /** Callback to fire when a column resize completes. */
94
- onColumnResized?: (columnId: string, width: number) => void;
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
- hasCellSelection: boolean;
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,7 +15,6 @@ 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;
20
19
  className?: string;
21
20
  entityLabelPlural: string;
@@ -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, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './types';
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
- /** Single filter value: text (string), multi-select (string[]), people (UserLike), or date range. */
22
- export type FilterValue = string | string[] | UserLike | IDateFilterValue;
23
- /** Unified filter model: field id -> filter value. Use FilterValue for type-safe access. */
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;
@@ -198,8 +203,6 @@ export interface IOGridProps<T> {
198
203
  };
199
204
  entityLabelPlural?: string;
200
205
  className?: string;
201
- /** @deprecated Render your title outside the OGrid component. Will be removed in next major. */
202
- title?: ReactNode;
203
206
  /** Where the column chooser renders.
204
207
  * - `true` or `'toolbar'` (default): column chooser button in the toolbar strip.
205
208
  * - `'sidebar'`: column chooser only available via the sidebar columns panel.
@@ -260,14 +263,10 @@ export interface IOGridDataGridProps<T> {
260
263
  selectedRows?: Set<RowId>;
261
264
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
262
265
  statusBar?: IStatusBarProps;
263
- multiSelectFilters: Record<string, string[]>;
264
- onMultiSelectFilterChange: (key: string, values: string[]) => void;
265
- textFilters?: Record<string, string>;
266
- onTextFilterChange?: (key: string, value: string) => void;
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;
266
+ /** Unified filter model (discriminated union values). */
267
+ filters: IFilters;
268
+ /** Single callback for all filter changes. Pass undefined to clear. */
269
+ onFilterChange: (key: string, value: FilterValue | undefined) => void;
271
270
  filterOptions: Record<string, string[]>;
272
271
  loadingFilterOptions: Record<string, boolean>;
273
272
  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, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
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
- textFilters?: Record<string, string>;
13
- onTextFilterChange?: (key: string, value: string) => void;
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
- multiSelectFilters: Record<string, string[]>;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "OGrid core – framework-agnostic types, hooks, and utilities for OGrid data tables.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",