@alaarab/ogrid-react 2.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/esm/components/BaseInlineCellEditor.js +112 -0
- package/dist/esm/components/CellErrorBoundary.js +43 -0
- package/dist/esm/components/EmptyState.js +19 -0
- package/dist/esm/components/GridContextMenu.js +35 -0
- package/dist/esm/components/MarchingAntsOverlay.js +110 -0
- package/dist/esm/components/OGridLayout.js +91 -0
- package/dist/esm/components/SideBar.js +122 -0
- package/dist/esm/components/StatusBar.js +6 -0
- package/dist/esm/hooks/index.js +25 -0
- package/dist/esm/hooks/useActiveCell.js +62 -0
- package/dist/esm/hooks/useCellEditing.js +15 -0
- package/dist/esm/hooks/useCellSelection.js +327 -0
- package/dist/esm/hooks/useClipboard.js +161 -0
- package/dist/esm/hooks/useColumnChooserState.js +62 -0
- package/dist/esm/hooks/useColumnHeaderFilterState.js +180 -0
- package/dist/esm/hooks/useColumnResize.js +92 -0
- package/dist/esm/hooks/useContextMenu.js +21 -0
- package/dist/esm/hooks/useDataGridState.js +313 -0
- package/dist/esm/hooks/useDateFilterState.js +34 -0
- package/dist/esm/hooks/useDebounce.js +35 -0
- package/dist/esm/hooks/useFillHandle.js +195 -0
- package/dist/esm/hooks/useFilterOptions.js +40 -0
- package/dist/esm/hooks/useInlineCellEditorState.js +44 -0
- package/dist/esm/hooks/useKeyboardNavigation.js +419 -0
- package/dist/esm/hooks/useLatestRef.js +11 -0
- package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
- package/dist/esm/hooks/useOGrid.js +465 -0
- package/dist/esm/hooks/usePeopleFilterState.js +68 -0
- package/dist/esm/hooks/useRichSelectState.js +58 -0
- package/dist/esm/hooks/useRowSelection.js +80 -0
- package/dist/esm/hooks/useSideBarState.js +39 -0
- package/dist/esm/hooks/useTableLayout.js +77 -0
- package/dist/esm/hooks/useTextFilterState.js +25 -0
- package/dist/esm/hooks/useUndoRedo.js +83 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/storybook/index.js +1 -0
- package/dist/esm/storybook/mockData.js +73 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/dataGridViewModel.js +220 -0
- package/dist/esm/utils/gridRowComparator.js +2 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
- package/dist/types/components/CellErrorBoundary.d.ts +25 -0
- package/dist/types/components/EmptyState.d.ts +26 -0
- package/dist/types/components/GridContextMenu.d.ts +18 -0
- package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
- package/dist/types/components/OGridLayout.d.ts +37 -0
- package/dist/types/components/SideBar.d.ts +30 -0
- package/dist/types/components/StatusBar.d.ts +24 -0
- package/dist/types/hooks/index.d.ts +48 -0
- package/dist/types/hooks/useActiveCell.d.ts +13 -0
- package/dist/types/hooks/useCellEditing.d.ts +16 -0
- package/dist/types/hooks/useCellSelection.d.ts +22 -0
- package/dist/types/hooks/useClipboard.d.ts +30 -0
- package/dist/types/hooks/useColumnChooserState.d.ts +27 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +73 -0
- package/dist/types/hooks/useColumnResize.d.ts +23 -0
- package/dist/types/hooks/useContextMenu.d.ts +19 -0
- package/dist/types/hooks/useDataGridState.d.ts +137 -0
- package/dist/types/hooks/useDateFilterState.d.ts +19 -0
- package/dist/types/hooks/useDebounce.d.ts +9 -0
- package/dist/types/hooks/useFillHandle.d.ts +33 -0
- package/dist/types/hooks/useFilterOptions.d.ts +16 -0
- package/dist/types/hooks/useInlineCellEditorState.d.ts +24 -0
- package/dist/types/hooks/useKeyboardNavigation.d.ts +47 -0
- package/dist/types/hooks/useLatestRef.d.ts +6 -0
- package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
- package/dist/types/hooks/useOGrid.d.ts +52 -0
- package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
- package/dist/types/hooks/useRichSelectState.d.ts +22 -0
- package/dist/types/hooks/useRowSelection.d.ts +22 -0
- package/dist/types/hooks/useSideBarState.d.ts +20 -0
- package/dist/types/hooks/useTableLayout.d.ts +27 -0
- package/dist/types/hooks/useTextFilterState.d.ts +16 -0
- package/dist/types/hooks/useUndoRedo.d.ts +23 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/storybook/index.d.ts +2 -0
- package/dist/types/storybook/mockData.d.ts +37 -0
- package/dist/types/types/columnTypes.d.ts +25 -0
- package/dist/types/types/dataGridTypes.d.ts +152 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +161 -0
- package/dist/types/utils/gridRowComparator.d.ts +2 -0
- package/dist/types/utils/index.d.ts +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
const DEFAULT_PANELS = ['columns', 'filters'];
|
|
3
|
+
/**
|
|
4
|
+
* Manages side bar panel state: enabled panels, active panel, position, and toggle/close handlers.
|
|
5
|
+
* @param params - Side bar config (boolean, ISideBarDef, or undefined).
|
|
6
|
+
* @returns Enabled flag, active panel, setters, panel list, position, open state, toggle, and close.
|
|
7
|
+
*/
|
|
8
|
+
export function useSideBarState(params) {
|
|
9
|
+
const { config } = params;
|
|
10
|
+
const isEnabled = config != null && config !== false;
|
|
11
|
+
const parsed = useMemo(() => {
|
|
12
|
+
if (!isEnabled || config === true) {
|
|
13
|
+
return { panels: DEFAULT_PANELS, position: 'right', defaultPanel: null };
|
|
14
|
+
}
|
|
15
|
+
const def = config;
|
|
16
|
+
return {
|
|
17
|
+
panels: def.panels ?? DEFAULT_PANELS,
|
|
18
|
+
position: def.position ?? 'right',
|
|
19
|
+
defaultPanel: def.defaultPanel ?? null,
|
|
20
|
+
};
|
|
21
|
+
}, [isEnabled, config]);
|
|
22
|
+
const [activePanel, setActivePanel] = useState(parsed.defaultPanel);
|
|
23
|
+
const toggle = useCallback((panel) => {
|
|
24
|
+
setActivePanel((prev) => (prev === panel ? null : panel));
|
|
25
|
+
}, []);
|
|
26
|
+
const close = useCallback(() => {
|
|
27
|
+
setActivePanel(null);
|
|
28
|
+
}, []);
|
|
29
|
+
return {
|
|
30
|
+
isEnabled,
|
|
31
|
+
activePanel,
|
|
32
|
+
setActivePanel,
|
|
33
|
+
panels: parsed.panels,
|
|
34
|
+
position: parsed.position,
|
|
35
|
+
isOpen: activePanel !== null,
|
|
36
|
+
toggle,
|
|
37
|
+
close,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING } from '@alaarab/ogrid-core';
|
|
3
|
+
/**
|
|
4
|
+
* Manages table layout: container width measurement, column sizing overrides,
|
|
5
|
+
* min/desired table width calculations.
|
|
6
|
+
*/
|
|
7
|
+
export function useTableLayout(params) {
|
|
8
|
+
const { wrapperRef, visibleCols, flatColumns, hasCheckboxCol, initialColumnWidths, onColumnResized, } = params;
|
|
9
|
+
// --- Container width measurement via ResizeObserver ---
|
|
10
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const el = wrapperRef.current;
|
|
13
|
+
if (!el)
|
|
14
|
+
return;
|
|
15
|
+
const measure = () => {
|
|
16
|
+
const rect = el.getBoundingClientRect();
|
|
17
|
+
const cs = window.getComputedStyle(el);
|
|
18
|
+
const borderX = (parseFloat(cs.borderLeftWidth || '0') || 0) +
|
|
19
|
+
(parseFloat(cs.borderRightWidth || '0') || 0);
|
|
20
|
+
setContainerWidth(Math.max(0, rect.width - borderX));
|
|
21
|
+
};
|
|
22
|
+
const ro = new ResizeObserver(measure);
|
|
23
|
+
ro.observe(el);
|
|
24
|
+
measure();
|
|
25
|
+
return () => ro.disconnect();
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, []); // wrapperRef excluded — refs are stable
|
|
28
|
+
// --- Column sizing overrides state ---
|
|
29
|
+
const [columnSizingOverrides, setColumnSizingOverrides] = useState(() => {
|
|
30
|
+
if (!initialColumnWidths)
|
|
31
|
+
return {};
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const [id, width] of Object.entries(initialColumnWidths)) {
|
|
34
|
+
result[id] = { widthPx: width };
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
});
|
|
38
|
+
// --- Minimum table width calculation ---
|
|
39
|
+
const minTableWidth = useMemo(() => {
|
|
40
|
+
const checkboxW = hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0;
|
|
41
|
+
return visibleCols.reduce((sum, c) => sum + (c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH) + CELL_PADDING, checkboxW);
|
|
42
|
+
}, [visibleCols, hasCheckboxCol]);
|
|
43
|
+
// --- Cleanup effect: remove overrides for columns that no longer exist ---
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const colIds = new Set(flatColumns.map((c) => c.columnId));
|
|
46
|
+
setColumnSizingOverrides((prev) => {
|
|
47
|
+
const next = { ...prev };
|
|
48
|
+
let changed = false;
|
|
49
|
+
for (const id of Object.keys(next)) {
|
|
50
|
+
if (!colIds.has(id)) {
|
|
51
|
+
delete next[id];
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return changed ? next : prev;
|
|
56
|
+
});
|
|
57
|
+
}, [flatColumns]);
|
|
58
|
+
// --- Desired table width calculation ---
|
|
59
|
+
const desiredTableWidth = useMemo(() => {
|
|
60
|
+
const checkboxW = hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0;
|
|
61
|
+
return visibleCols.reduce((sum, c) => {
|
|
62
|
+
const override = columnSizingOverrides[c.columnId];
|
|
63
|
+
const w = override
|
|
64
|
+
? override.widthPx
|
|
65
|
+
: (c.idealWidth ?? c.defaultWidth ?? c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
|
|
66
|
+
return sum + Math.max(c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, w) + CELL_PADDING;
|
|
67
|
+
}, checkboxW);
|
|
68
|
+
}, [visibleCols, columnSizingOverrides, hasCheckboxCol]);
|
|
69
|
+
return {
|
|
70
|
+
containerWidth,
|
|
71
|
+
minTableWidth,
|
|
72
|
+
desiredTableWidth,
|
|
73
|
+
columnSizingOverrides,
|
|
74
|
+
setColumnSizingOverrides,
|
|
75
|
+
onColumnResized,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text filter state sub-hook for column header filters.
|
|
3
|
+
* Manages temporary text value and apply/clear handlers.
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
6
|
+
export function useTextFilterState(params) {
|
|
7
|
+
const { textValue = '', onTextChange, isFilterOpen } = params;
|
|
8
|
+
const [tempTextValue, setTempTextValue] = useState(textValue);
|
|
9
|
+
// Sync temp state when popover opens
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (isFilterOpen) {
|
|
12
|
+
setTempTextValue(textValue);
|
|
13
|
+
}
|
|
14
|
+
}, [isFilterOpen, textValue]);
|
|
15
|
+
const handleTextApply = useCallback(() => {
|
|
16
|
+
onTextChange?.(tempTextValue.trim());
|
|
17
|
+
}, [onTextChange, tempTextValue]);
|
|
18
|
+
const handleTextClear = useCallback(() => setTempTextValue(''), []);
|
|
19
|
+
return {
|
|
20
|
+
tempTextValue,
|
|
21
|
+
setTempTextValue,
|
|
22
|
+
handleTextApply,
|
|
23
|
+
handleTextClear,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps onCellValueChanged with an undo/redo history stack.
|
|
4
|
+
* Supports batch operations: changes between beginBatch/endBatch are one undo step.
|
|
5
|
+
*/
|
|
6
|
+
export function useUndoRedo(params) {
|
|
7
|
+
const { onCellValueChanged, maxUndoDepth = 100 } = params;
|
|
8
|
+
// Each history entry is an array of events (batch). Single edits are [event].
|
|
9
|
+
const historyRef = useRef([]);
|
|
10
|
+
const redoStackRef = useRef([]);
|
|
11
|
+
const batchRef = useRef(null);
|
|
12
|
+
const [historyLength, setHistoryLength] = useState(0);
|
|
13
|
+
const [redoLength, setRedoLength] = useState(0);
|
|
14
|
+
const wrapped = useCallback((event) => {
|
|
15
|
+
if (!onCellValueChanged)
|
|
16
|
+
return;
|
|
17
|
+
if (batchRef.current !== null) {
|
|
18
|
+
// Accumulate into the current batch — don't push to history yet
|
|
19
|
+
batchRef.current.push(event);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
historyRef.current = [...historyRef.current, [event]].slice(-maxUndoDepth);
|
|
23
|
+
redoStackRef.current = [];
|
|
24
|
+
setHistoryLength(historyRef.current.length);
|
|
25
|
+
setRedoLength(0);
|
|
26
|
+
}
|
|
27
|
+
onCellValueChanged(event);
|
|
28
|
+
}, [onCellValueChanged, maxUndoDepth]);
|
|
29
|
+
const beginBatch = useCallback(() => {
|
|
30
|
+
batchRef.current = [];
|
|
31
|
+
}, []);
|
|
32
|
+
const endBatch = useCallback(() => {
|
|
33
|
+
const batch = batchRef.current;
|
|
34
|
+
batchRef.current = null;
|
|
35
|
+
if (!batch || batch.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
historyRef.current = [...historyRef.current, batch].slice(-maxUndoDepth);
|
|
38
|
+
redoStackRef.current = [];
|
|
39
|
+
setHistoryLength(historyRef.current.length);
|
|
40
|
+
setRedoLength(0);
|
|
41
|
+
}, [maxUndoDepth]);
|
|
42
|
+
const undo = useCallback(() => {
|
|
43
|
+
if (!onCellValueChanged || historyRef.current.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
const lastBatch = historyRef.current[historyRef.current.length - 1];
|
|
46
|
+
historyRef.current = historyRef.current.slice(0, -1);
|
|
47
|
+
redoStackRef.current = [...redoStackRef.current, lastBatch];
|
|
48
|
+
setHistoryLength(historyRef.current.length);
|
|
49
|
+
setRedoLength(redoStackRef.current.length);
|
|
50
|
+
// Revert in reverse order so multi-cell undo is applied correctly
|
|
51
|
+
for (let i = lastBatch.length - 1; i >= 0; i--) {
|
|
52
|
+
const ev = lastBatch[i];
|
|
53
|
+
onCellValueChanged({
|
|
54
|
+
...ev,
|
|
55
|
+
oldValue: ev.newValue,
|
|
56
|
+
newValue: ev.oldValue,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}, [onCellValueChanged]);
|
|
60
|
+
const redo = useCallback(() => {
|
|
61
|
+
if (!onCellValueChanged || redoStackRef.current.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const nextBatch = redoStackRef.current[redoStackRef.current.length - 1];
|
|
64
|
+
redoStackRef.current = redoStackRef.current.slice(0, -1);
|
|
65
|
+
historyRef.current = [...historyRef.current, nextBatch];
|
|
66
|
+
setRedoLength(redoStackRef.current.length);
|
|
67
|
+
setHistoryLength(historyRef.current.length);
|
|
68
|
+
// Replay in original order
|
|
69
|
+
for (const ev of nextBatch) {
|
|
70
|
+
onCellValueChanged(ev);
|
|
71
|
+
}
|
|
72
|
+
}, [onCellValueChanged]);
|
|
73
|
+
return {
|
|
74
|
+
onCellValueChanged: onCellValueChanged ? wrapped : undefined,
|
|
75
|
+
undo,
|
|
76
|
+
redo,
|
|
77
|
+
canUndo: historyLength > 0,
|
|
78
|
+
canRedo: redoLength > 0,
|
|
79
|
+
beginBatch,
|
|
80
|
+
endBatch,
|
|
81
|
+
maxUndoDepth,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Constants (re-exported from core)
|
|
2
|
+
export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
3
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
4
|
+
// Hooks
|
|
5
|
+
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useLatestRef, } from './hooks';
|
|
6
|
+
// Components
|
|
7
|
+
export { OGridLayout } from './components/OGridLayout';
|
|
8
|
+
export { StatusBar } from './components/StatusBar';
|
|
9
|
+
export { BaseInlineCellEditor, editorWrapperStyle, editorInputStyle, richSelectWrapperStyle, richSelectDropdownStyle, richSelectOptionStyle, richSelectOptionHighlightedStyle, richSelectNoMatchesStyle, selectEditorStyle, } from './components/BaseInlineCellEditor';
|
|
10
|
+
export { GridContextMenu } from './components/GridContextMenu';
|
|
11
|
+
export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
12
|
+
export { SideBar } from './components/SideBar';
|
|
13
|
+
export { CellErrorBoundary } from './components/CellErrorBoundary';
|
|
14
|
+
export { EmptyState } from './components/EmptyState';
|
|
15
|
+
// Utilities
|
|
16
|
+
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, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, areGridRowPropsEqual, } from './utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { storyRows, storyGetRowId, noop, editableInitialRows, editableColumns, NotesPopupEditor, spreadsheetRows, spreadsheetColumns, departmentFilterOptions, statusFilterOptions, } from './mockData';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
export const storyRows = [
|
|
4
|
+
{ id: '1', name: 'Alpha', status: 'Active', owner: 'alice@test.com' },
|
|
5
|
+
{ id: '2', name: 'Beta', status: 'Closed', owner: 'bob@test.com' },
|
|
6
|
+
{ id: '3', name: 'Gamma', status: 'Active', owner: 'carol@test.com' },
|
|
7
|
+
{ id: '4', name: 'Delta', status: 'Planning', owner: 'dave@test.com' },
|
|
8
|
+
];
|
|
9
|
+
export const storyGetRowId = (r) => r.id;
|
|
10
|
+
export const noop = () => { };
|
|
11
|
+
export const editableInitialRows = [
|
|
12
|
+
{ id: '1', name: 'Alpha', status: 'Active', approved: false },
|
|
13
|
+
{ id: '2', name: 'Beta', status: 'Closed', approved: true },
|
|
14
|
+
{ id: '3', name: 'Gamma', status: 'Planning', approved: false },
|
|
15
|
+
];
|
|
16
|
+
export const editableColumns = [
|
|
17
|
+
{
|
|
18
|
+
columnId: 'name',
|
|
19
|
+
name: 'Name',
|
|
20
|
+
editable: true,
|
|
21
|
+
cellEditor: 'text',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
columnId: 'status',
|
|
25
|
+
name: 'Status',
|
|
26
|
+
editable: true,
|
|
27
|
+
cellEditor: 'select',
|
|
28
|
+
cellEditorParams: { values: ['Active', 'Closed', 'Planning'] },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
columnId: 'approved',
|
|
32
|
+
name: 'Approved',
|
|
33
|
+
editable: true,
|
|
34
|
+
cellEditor: 'checkbox',
|
|
35
|
+
valueFormatter: (v) => (v === true ? 'Yes' : 'No'),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
export function NotesPopupEditor({ value, onValueChange, onCommit, onCancel }) {
|
|
39
|
+
const [local, setLocal] = React.useState(String(value ?? ''));
|
|
40
|
+
return (_jsxs("div", { style: { padding: 8, minWidth: 200 }, children: [_jsx("textarea", { value: local, onChange: (e) => setLocal(e.target.value), onBlur: () => {
|
|
41
|
+
onValueChange(local);
|
|
42
|
+
onCommit();
|
|
43
|
+
}, rows: 3, style: { width: '100%', marginBottom: 8 }, "data-testid": "notes-editor" }), _jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx("button", { type: "button", onClick: () => { onValueChange(local); onCommit(); }, children: "Save" }), _jsx("button", { type: "button", onClick: onCancel, children: "Cancel" })] })] }));
|
|
44
|
+
}
|
|
45
|
+
export const spreadsheetRows = [
|
|
46
|
+
{ id: '1', name: 'Alice Johnson', department: 'Engineering', salary: 125000, startDate: '2021-03-15', status: 'Active', email: 'alice@company.com' },
|
|
47
|
+
{ id: '2', name: 'Bob Smith', department: 'Marketing', salary: 95000, startDate: '2020-07-01', status: 'Active', email: 'bob@company.com' },
|
|
48
|
+
{ id: '3', name: 'Carol Williams', department: 'Engineering', salary: 140000, startDate: '2019-11-20', status: 'Active', email: 'carol@company.com' },
|
|
49
|
+
{ id: '4', name: 'Dave Brown', department: 'Sales', salary: 85000, startDate: '2022-01-10', status: 'On Leave', email: 'dave@company.com' },
|
|
50
|
+
{ id: '5', name: 'Eve Davis', department: 'Engineering', salary: 155000, startDate: '2018-05-22', status: 'Active', email: 'eve@company.com' },
|
|
51
|
+
{ id: '6', name: 'Frank Miller', department: 'Marketing', salary: 88000, startDate: '2023-02-14', status: 'Active', email: 'frank@company.com' },
|
|
52
|
+
{ id: '7', name: 'Grace Lee', department: 'Sales', salary: 92000, startDate: '2021-08-30', status: 'Active', email: 'grace@company.com' },
|
|
53
|
+
{ id: '8', name: 'Henry Wilson', department: 'Engineering', salary: 130000, startDate: '2020-04-12', status: 'Inactive', email: 'henry@company.com' },
|
|
54
|
+
{ id: '9', name: 'Iris Taylor', department: 'HR', salary: 78000, startDate: '2022-09-05', status: 'Active', email: 'iris@company.com' },
|
|
55
|
+
{ id: '10', name: 'Jack Anderson', department: 'Engineering', salary: 145000, startDate: '2019-06-18', status: 'Active', email: 'jack@company.com' },
|
|
56
|
+
];
|
|
57
|
+
export const spreadsheetColumns = [
|
|
58
|
+
{ columnId: 'name', name: 'Employee Name', sortable: true, minWidth: 160, filterable: { type: 'text' } },
|
|
59
|
+
{ columnId: 'department', name: 'Department', sortable: true, filterable: { type: 'multiSelect' } },
|
|
60
|
+
{
|
|
61
|
+
columnId: 'salary',
|
|
62
|
+
name: 'Salary',
|
|
63
|
+
sortable: true,
|
|
64
|
+
minWidth: 100,
|
|
65
|
+
valueFormatter: (v) => typeof v === 'number' ? `$${v.toLocaleString()}` : '',
|
|
66
|
+
cellStyle: { textAlign: 'right', fontVariantNumeric: 'tabular-nums' },
|
|
67
|
+
},
|
|
68
|
+
{ columnId: 'startDate', name: 'Start Date', sortable: true, minWidth: 110 },
|
|
69
|
+
{ columnId: 'status', name: 'Status', sortable: true, filterable: { type: 'multiSelect' } },
|
|
70
|
+
{ columnId: 'email', name: 'Email', sortable: true, minWidth: 180 },
|
|
71
|
+
];
|
|
72
|
+
export const departmentFilterOptions = ['Engineering', 'Marketing', 'Sales', 'HR'];
|
|
73
|
+
export const statusFilterOptions = ['Active', 'On Leave', 'Inactive'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View model helpers for DataGridTable. Core owns the logic; UI packages only render.
|
|
3
|
+
*/
|
|
4
|
+
import { getCellValue, isInSelectionRange } from '@alaarab/ogrid-core';
|
|
5
|
+
/**
|
|
6
|
+
* Returns ColumnHeaderFilter props from column def and grid filter/sort state.
|
|
7
|
+
* Use in Fluent/Material/Radix DataGridTable instead of createHeaderWithFilter.
|
|
8
|
+
*/
|
|
9
|
+
export function getHeaderFilterConfig(col, input) {
|
|
10
|
+
const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
|
|
11
|
+
const filterType = (filterable?.type ?? 'none');
|
|
12
|
+
const filterField = filterable?.filterField ?? col.columnId;
|
|
13
|
+
const sortable = col.sortable !== false;
|
|
14
|
+
const filterValue = input.filters[filterField];
|
|
15
|
+
const base = {
|
|
16
|
+
columnKey: col.columnId,
|
|
17
|
+
columnName: col.name,
|
|
18
|
+
filterType,
|
|
19
|
+
isSorted: input.sortBy === col.columnId,
|
|
20
|
+
isSortedDescending: input.sortBy === col.columnId && input.sortDirection === 'desc',
|
|
21
|
+
onSort: sortable ? () => input.onColumnSort(col.columnId) : undefined,
|
|
22
|
+
};
|
|
23
|
+
if (filterType === 'text') {
|
|
24
|
+
return {
|
|
25
|
+
...base,
|
|
26
|
+
textValue: filterValue?.type === 'text' ? filterValue.value : '',
|
|
27
|
+
onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (filterType === 'people') {
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
|
|
34
|
+
onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
|
|
35
|
+
peopleSearch: input.peopleSearch,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (filterType === 'multiSelect') {
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
options: input.filterOptions[filterField] ?? [],
|
|
42
|
+
isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
|
|
43
|
+
selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
|
|
44
|
+
onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (filterType === 'date') {
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
|
|
51
|
+
onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
|
|
58
|
+
* and to apply isActive, isInRange, etc. without duplicating the boolean logic.
|
|
59
|
+
*/
|
|
60
|
+
export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
|
|
61
|
+
const rowId = input.getRowId(item);
|
|
62
|
+
const globalColIndex = colIdx + input.colOffset;
|
|
63
|
+
const colEditable = col.editable === true ||
|
|
64
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
65
|
+
const canEditInline = input.editable !== false &&
|
|
66
|
+
!!colEditable &&
|
|
67
|
+
!!input.onCellValueChanged &&
|
|
68
|
+
typeof col.cellEditor !== 'function';
|
|
69
|
+
const canEditPopup = input.editable !== false &&
|
|
70
|
+
!!colEditable &&
|
|
71
|
+
!!input.onCellValueChanged &&
|
|
72
|
+
typeof col.cellEditor === 'function' &&
|
|
73
|
+
col.cellEditorPopup !== false;
|
|
74
|
+
const canEditAny = canEditInline || canEditPopup;
|
|
75
|
+
const isEditing = input.editingCell?.rowId === rowId &&
|
|
76
|
+
input.editingCell?.columnId === col.columnId;
|
|
77
|
+
const isActive = input.activeCell?.rowIndex === rowIndex &&
|
|
78
|
+
input.activeCell?.columnIndex === globalColIndex;
|
|
79
|
+
const isInRange = input.selectionRange != null &&
|
|
80
|
+
isInSelectionRange(input.selectionRange, rowIndex, colIdx);
|
|
81
|
+
const isInCutRange = input.cutRange != null &&
|
|
82
|
+
isInSelectionRange(input.cutRange, rowIndex, colIdx);
|
|
83
|
+
const isInCopyRange = input.copyRange != null &&
|
|
84
|
+
isInSelectionRange(input.copyRange, rowIndex, colIdx);
|
|
85
|
+
const isSelectionEndCell = !input.isDragging &&
|
|
86
|
+
input.copyRange == null &&
|
|
87
|
+
input.cutRange == null &&
|
|
88
|
+
input.selectionRange != null &&
|
|
89
|
+
rowIndex === input.selectionRange.endRow &&
|
|
90
|
+
colIdx === input.selectionRange.endCol;
|
|
91
|
+
const isPinned = col.pinned != null;
|
|
92
|
+
const pinnedSide = col.pinned ?? undefined;
|
|
93
|
+
let mode = 'display';
|
|
94
|
+
let editorType;
|
|
95
|
+
let value;
|
|
96
|
+
if (isEditing && canEditInline) {
|
|
97
|
+
mode = 'editing-inline';
|
|
98
|
+
if (col.cellEditor === 'text' ||
|
|
99
|
+
col.cellEditor === 'select' ||
|
|
100
|
+
col.cellEditor === 'checkbox' ||
|
|
101
|
+
col.cellEditor === 'richSelect' ||
|
|
102
|
+
col.cellEditor === 'date') {
|
|
103
|
+
editorType = col.cellEditor;
|
|
104
|
+
}
|
|
105
|
+
else if (col.type === 'date') {
|
|
106
|
+
editorType = 'date';
|
|
107
|
+
}
|
|
108
|
+
else if (col.type === 'boolean') {
|
|
109
|
+
editorType = 'checkbox';
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
editorType = 'text';
|
|
113
|
+
}
|
|
114
|
+
value = getCellValue(item, col);
|
|
115
|
+
}
|
|
116
|
+
else if (isEditing && canEditPopup && typeof col.cellEditor === 'function') {
|
|
117
|
+
mode = 'editing-popover';
|
|
118
|
+
value = getCellValue(item, col);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
value = getCellValue(item, col);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
mode,
|
|
125
|
+
editorType,
|
|
126
|
+
value,
|
|
127
|
+
isActive,
|
|
128
|
+
isInRange,
|
|
129
|
+
isInCutRange,
|
|
130
|
+
isInCopyRange,
|
|
131
|
+
isSelectionEndCell,
|
|
132
|
+
canEditAny,
|
|
133
|
+
isPinned,
|
|
134
|
+
pinnedSide,
|
|
135
|
+
globalColIndex,
|
|
136
|
+
rowId,
|
|
137
|
+
rowIndex,
|
|
138
|
+
displayValue: value,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// --- Cell rendering helpers (reduce DataGridTable view-layer duplication) ---
|
|
142
|
+
/**
|
|
143
|
+
* Resolves display content for a cell in display mode.
|
|
144
|
+
* Handles the renderCell → valueFormatter → String() fallback chain.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveCellDisplayContent(col, item, displayValue) {
|
|
147
|
+
if (col.renderCell)
|
|
148
|
+
return col.renderCell(item);
|
|
149
|
+
if (col.valueFormatter)
|
|
150
|
+
return col.valueFormatter(displayValue, item);
|
|
151
|
+
if (displayValue == null)
|
|
152
|
+
return null;
|
|
153
|
+
if (col.type === 'date') {
|
|
154
|
+
const d = new Date(String(displayValue));
|
|
155
|
+
if (!Number.isNaN(d.getTime()))
|
|
156
|
+
return d.toLocaleDateString();
|
|
157
|
+
}
|
|
158
|
+
if (col.type === 'boolean') {
|
|
159
|
+
return displayValue ? 'True' : 'False';
|
|
160
|
+
}
|
|
161
|
+
return String(displayValue);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolves the cellStyle from a column def, handling both function and static values.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveCellStyle(col, item) {
|
|
167
|
+
if (!col.cellStyle)
|
|
168
|
+
return undefined;
|
|
169
|
+
return typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Builds props for InlineCellEditor. Shared across all UI packages.
|
|
173
|
+
*/
|
|
174
|
+
export function buildInlineEditorProps(item, col, descriptor, callbacks) {
|
|
175
|
+
return {
|
|
176
|
+
value: descriptor.value,
|
|
177
|
+
item,
|
|
178
|
+
column: col,
|
|
179
|
+
rowIndex: descriptor.rowIndex,
|
|
180
|
+
editorType: (descriptor.editorType ?? 'text'),
|
|
181
|
+
onCommit: (newValue) => callbacks.commitCellEdit(item, col.columnId, descriptor.value, newValue, descriptor.rowIndex, descriptor.globalColIndex),
|
|
182
|
+
onCancel: () => callbacks.setEditingCell(null),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Builds ICellEditorProps for custom popover editors. Shared across all UI packages.
|
|
187
|
+
*/
|
|
188
|
+
export function buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, callbacks) {
|
|
189
|
+
const oldValue = descriptor.value;
|
|
190
|
+
const displayValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
191
|
+
return {
|
|
192
|
+
value: displayValue,
|
|
193
|
+
onValueChange: callbacks.setPendingEditorValue,
|
|
194
|
+
onCommit: () => {
|
|
195
|
+
const newValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
196
|
+
callbacks.commitCellEdit(item, col.columnId, oldValue, newValue, descriptor.rowIndex, descriptor.globalColIndex);
|
|
197
|
+
},
|
|
198
|
+
onCancel: callbacks.cancelPopoverEdit,
|
|
199
|
+
item,
|
|
200
|
+
column: col,
|
|
201
|
+
cellEditorParams: col.cellEditorParams,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function getCellInteractionProps(descriptor, columnId, handlers) {
|
|
205
|
+
return {
|
|
206
|
+
'data-row-index': descriptor.rowIndex,
|
|
207
|
+
'data-col-index': descriptor.globalColIndex,
|
|
208
|
+
...(descriptor.isInRange ? { 'data-in-range': 'true' } : {}),
|
|
209
|
+
tabIndex: descriptor.isActive ? 0 : -1,
|
|
210
|
+
onMouseDown: (e) => handlers.handleCellMouseDown(e, descriptor.rowIndex, descriptor.globalColIndex),
|
|
211
|
+
onClick: () => handlers.setActiveCell({ rowIndex: descriptor.rowIndex, columnIndex: descriptor.globalColIndex }),
|
|
212
|
+
onContextMenu: handlers.handleCellContextMenu,
|
|
213
|
+
...(descriptor.canEditAny
|
|
214
|
+
? {
|
|
215
|
+
role: 'button',
|
|
216
|
+
onDoubleClick: () => handlers.setEditingCell({ rowId: descriptor.rowId, columnId }),
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Shared utilities re-exported from core
|
|
2
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, } from '@alaarab/ogrid-core';
|
|
3
|
+
// React-specific utilities (not in core)
|
|
4
|
+
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
|
|
5
|
+
export { areGridRowPropsEqual, isRowInRange } from './gridRowComparator';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IColumnDef } from '../types';
|
|
3
|
+
export declare const editorWrapperStyle: React.CSSProperties;
|
|
4
|
+
export declare const editorInputStyle: React.CSSProperties;
|
|
5
|
+
export declare const richSelectWrapperStyle: React.CSSProperties;
|
|
6
|
+
export declare const richSelectDropdownStyle: React.CSSProperties;
|
|
7
|
+
export declare const richSelectOptionStyle: React.CSSProperties;
|
|
8
|
+
export declare const richSelectOptionHighlightedStyle: React.CSSProperties;
|
|
9
|
+
export declare const richSelectNoMatchesStyle: React.CSSProperties;
|
|
10
|
+
export declare const selectEditorStyle: React.CSSProperties;
|
|
11
|
+
export interface BaseInlineCellEditorProps<T> {
|
|
12
|
+
value: unknown;
|
|
13
|
+
item: T;
|
|
14
|
+
column: IColumnDef<T>;
|
|
15
|
+
rowIndex: number;
|
|
16
|
+
editorType: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
|
|
17
|
+
onCommit: (value: unknown) => void;
|
|
18
|
+
onCancel: () => void;
|
|
19
|
+
/** Framework-specific checkbox renderer */
|
|
20
|
+
renderCheckbox: (checked: boolean, onCommit: (value: boolean) => void, onCancel: () => void) => React.ReactNode;
|
|
21
|
+
/** Framework-specific select renderer */
|
|
22
|
+
renderSelect: (value: unknown, values: unknown[], onCommit: (value: unknown) => void, onCancel: () => void) => React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Base inline cell editor with shared logic for all editor types except checkbox and select
|
|
26
|
+
* (which are framework-specific). Used by all 3 UI packages to avoid duplication.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* - Radix: Pass Radix Checkbox/native select via render props
|
|
30
|
+
* - Fluent: Pass Fluent Checkbox/Select via render props
|
|
31
|
+
* - Material: Pass MUI Checkbox/Select via render props
|
|
32
|
+
*/
|
|
33
|
+
export declare function BaseInlineCellEditor<T>(props: BaseInlineCellEditorProps<T>): React.ReactElement;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props for the CellErrorBoundary component.
|
|
4
|
+
*/
|
|
5
|
+
export interface CellErrorBoundaryProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
8
|
+
fallback?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
interface CellErrorBoundaryState {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Error boundary for cell renderers and custom editors.
|
|
15
|
+
* Prevents a runtime error in a cell from crashing the entire grid.
|
|
16
|
+
*/
|
|
17
|
+
export declare class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, CellErrorBoundaryState> {
|
|
18
|
+
constructor(props: CellErrorBoundaryProps);
|
|
19
|
+
static getDerivedStateFromError(): CellErrorBoundaryState;
|
|
20
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
21
|
+
componentDidUpdate(prevProps: CellErrorBoundaryProps): void;
|
|
22
|
+
resetErrorBoundary(): void;
|
|
23
|
+
render(): React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props for the EmptyState component.
|
|
4
|
+
* Used to render "No results found" messages in DataGridTable.
|
|
5
|
+
*/
|
|
6
|
+
export interface EmptyStateProps {
|
|
7
|
+
/** Custom message override */
|
|
8
|
+
message?: React.ReactNode;
|
|
9
|
+
/** Whether filters are currently active */
|
|
10
|
+
hasActiveFilters?: boolean;
|
|
11
|
+
/** Called when user clicks "clear all filters" link */
|
|
12
|
+
onClearAll?: () => void;
|
|
13
|
+
/** Custom render function (overrides default rendering) */
|
|
14
|
+
render?: () => React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Headless empty state component with default rendering logic.
|
|
18
|
+
* Framework-specific wrappers provide styling.
|
|
19
|
+
*
|
|
20
|
+
* Default behavior:
|
|
21
|
+
* - Shows "No results found" title
|
|
22
|
+
* - If hasActiveFilters=true: shows "clear all filters" link
|
|
23
|
+
* - If message provided: shows custom message
|
|
24
|
+
* - If render provided: uses custom renderer
|
|
25
|
+
*/
|
|
26
|
+
export declare function EmptyState(props: EmptyStateProps): React.ReactElement;
|