@alaarab/ogrid-react-material 2.0.11 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/esm/ColumnChooser/ColumnChooser.js +2 -2
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +11 -37
- package/dist/esm/DataGridTable/DataGridTable.js +81 -111
- package/dist/esm/DataGridTable/DropIndicator.js +15 -0
- package/dist/esm/DataGridTable/EmptyState.js +6 -0
- package/dist/esm/DataGridTable/LoadingOverlay.js +14 -0
- package/dist/esm/OGrid/OGrid.js +29 -0
- package/dist/esm/OGrid/index.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/types/ColumnChooser/ColumnChooser.d.ts +2 -8
- package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +2 -20
- package/dist/types/DataGridTable/DropIndicator.d.ts +7 -0
- package/dist/types/DataGridTable/EmptyState.d.ts +11 -0
- package/dist/types/DataGridTable/LoadingOverlay.d.ts +6 -0
- package/dist/types/{MaterialDataTable/MaterialDataTable.d.ts → OGrid/OGrid.d.ts} +0 -2
- package/dist/types/OGrid/index.d.ts +1 -0
- package/dist/types/PaginationControls/PaginationControls.d.ts +2 -10
- package/dist/types/index.d.ts +1 -1
- package/package.json +3 -3
- package/dist/esm/MaterialDataTable/MaterialDataTable.js +0 -19
- package/dist/esm/MaterialDataTable/index.js +0 -1
- package/dist/types/MaterialDataTable/index.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<strong>OGrid for Material
|
|
2
|
+
<strong>OGrid for React Material</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -39,7 +39,7 @@ export const ColumnChooser = (props) => {
|
|
|
39
39
|
py: 1,
|
|
40
40
|
borderBottom: 1,
|
|
41
41
|
borderColor: 'divider',
|
|
42
|
-
bgcolor: '
|
|
42
|
+
bgcolor: 'action.hover',
|
|
43
43
|
}, children: _jsxs(Typography, { variant: "subtitle2", fontWeight: 600, children: ["Select Columns (", visibleCount, " of ", totalCount, ")"] }) }), _jsx(Box, { sx: { maxHeight: 320, overflowY: 'auto', py: 0.5 }, children: columns.map((column) => (_jsx(Box, { sx: { px: 1.5, minHeight: 32, display: 'flex', alignItems: 'center' }, children: _jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: visibleColumns.has(column.columnId), onChange: handleCheckboxChange(column.columnId) }), label: _jsx(Typography, { variant: "body2", children: column.name }), sx: { m: 0 } }) }, column.columnId))) }), _jsxs(Box, { sx: {
|
|
44
44
|
display: 'flex',
|
|
45
45
|
justifyContent: 'flex-end',
|
|
@@ -48,6 +48,6 @@ export const ColumnChooser = (props) => {
|
|
|
48
48
|
py: 1,
|
|
49
49
|
borderTop: 1,
|
|
50
50
|
borderColor: 'divider',
|
|
51
|
-
bgcolor: '
|
|
51
|
+
bgcolor: 'action.hover',
|
|
52
52
|
}, children: [_jsx(Button, { size: "small", onClick: handleClearAll, sx: { textTransform: 'none' }, children: "Clear All" }), _jsx(Button, { size: "small", variant: "contained", onClick: handleSelectAll, sx: { textTransform: 'none' }, children: "Select All" })] })] })] }));
|
|
53
53
|
};
|
|
@@ -2,46 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { Popover, Tooltip, IconButton, Box, Typography } from '@mui/material';
|
|
4
4
|
import { ArrowUpward as ArrowUpwardIcon, ArrowDownward as ArrowDownwardIcon, SwapVert as SwapVertIcon, FilterList as FilterListIcon, } from '@mui/icons-material';
|
|
5
|
-
import { useColumnHeaderFilterState } from '@alaarab/ogrid-react';
|
|
5
|
+
import { useColumnHeaderFilterState, getColumnHeaderFilterStateParams, renderFilterContent, } from '@alaarab/ogrid-react';
|
|
6
6
|
import { TextFilterPopover } from './TextFilterPopover';
|
|
7
7
|
import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
|
|
8
8
|
import { PeopleFilterPopover } from './PeopleFilterPopover';
|
|
9
|
+
const materialRenderers = {
|
|
10
|
+
renderMultiSelect: (p) => (_jsx(MultiSelectFilterPopover, { searchText: p.searchText, onSearchChange: p.onSearchChange, options: p.options, filteredOptions: p.filteredOptions, selected: p.selected, onOptionToggle: p.onOptionToggle, onSelectAll: p.onSelectAll, onClearSelection: p.onClearSelection, onApply: p.onApply, isLoading: p.isLoading })),
|
|
11
|
+
renderText: (p) => (_jsx(TextFilterPopover, { value: p.value, onValueChange: p.onValueChange, onApply: p.onApply, onClear: p.onClear })),
|
|
12
|
+
renderPeople: (p) => (_jsx(PeopleFilterPopover, { selectedUser: p.selectedUser, searchText: p.searchText, onSearchChange: p.onSearchChange, suggestions: p.suggestions, isLoading: p.isLoading, onUserSelect: p.onUserSelect, onClearUser: p.onClearUser, inputRef: p.inputRef })),
|
|
13
|
+
renderDate: (p) => (_jsxs(Box, { sx: { p: 1.5, display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "From:" }), _jsx("input", { type: "date", value: p.tempDateFrom, onChange: (e) => p.setTempDateFrom(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "To:" }), _jsx("input", { type: "date", value: p.tempDateTo, onChange: (e) => p.setTempDateTo(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 0.5 }, children: [_jsx("button", { onClick: p.onClear, disabled: !p.tempDateFrom && !p.tempDateTo, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Clear" }), _jsx("button", { onClick: p.onApply, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Apply" })] })] })),
|
|
14
|
+
};
|
|
9
15
|
export const ColumnHeaderFilter = React.memo((props) => {
|
|
10
|
-
const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort,
|
|
11
|
-
const state = useColumnHeaderFilterState(
|
|
12
|
-
|
|
13
|
-
isSorted,
|
|
14
|
-
isSortedDescending,
|
|
15
|
-
onSort,
|
|
16
|
-
selectedValues,
|
|
17
|
-
onFilterChange,
|
|
18
|
-
options,
|
|
19
|
-
isLoadingOptions,
|
|
20
|
-
textValue,
|
|
21
|
-
onTextChange,
|
|
22
|
-
selectedUser,
|
|
23
|
-
onUserChange,
|
|
24
|
-
peopleSearch,
|
|
25
|
-
dateValue,
|
|
26
|
-
onDateChange,
|
|
27
|
-
});
|
|
28
|
-
const { headerRef, peopleInputRef, isFilterOpen, setFilterOpen, tempSelected, tempTextValue, setTempTextValue, searchText, setSearchText, filteredOptions, peopleSuggestions, isPeopleLoading, peopleSearchText, setPeopleSearchText, hasActiveFilter, popoverPosition, handlers, } = state;
|
|
29
|
-
const safeOptions = options ?? [];
|
|
30
|
-
const renderPopoverContent = () => {
|
|
31
|
-
if (filterType === 'multiSelect') {
|
|
32
|
-
return (_jsx(MultiSelectFilterPopover, { searchText: searchText, onSearchChange: setSearchText, options: safeOptions, filteredOptions: filteredOptions, selected: tempSelected, onOptionToggle: handlers.handleCheckboxChange, onSelectAll: handlers.handleSelectAll, onClearSelection: handlers.handleClearSelection, onApply: handlers.handleApplyMultiSelect, isLoading: isLoadingOptions }));
|
|
33
|
-
}
|
|
34
|
-
if (filterType === 'text') {
|
|
35
|
-
return (_jsx(TextFilterPopover, { value: tempTextValue, onValueChange: setTempTextValue, onApply: handlers.handleTextApply, onClear: handlers.handleTextClear }));
|
|
36
|
-
}
|
|
37
|
-
if (filterType === 'people') {
|
|
38
|
-
return (_jsx(PeopleFilterPopover, { selectedUser: selectedUser, searchText: peopleSearchText, onSearchChange: setPeopleSearchText, suggestions: peopleSuggestions, isLoading: isPeopleLoading, onUserSelect: handlers.handleUserSelect, onClearUser: handlers.handleClearUser, inputRef: peopleInputRef }));
|
|
39
|
-
}
|
|
40
|
-
if (filterType === 'date') {
|
|
41
|
-
return (_jsxs(Box, { sx: { p: 1.5, display: 'flex', flexDirection: 'column', gap: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "From:" }), _jsx("input", { type: "date", value: state.tempDateFrom, onChange: (e) => state.setTempDateFrom(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { minWidth: 36 }, children: "To:" }), _jsx("input", { type: "date", value: state.tempDateTo, onChange: (e) => state.setTempDateTo(e.target.value), style: { flex: 1, padding: '4px 6px' } })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 0.5 }, children: [_jsx("button", { onClick: handlers.handleDateClear, disabled: !state.tempDateFrom && !state.tempDateTo, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Clear" }), _jsx("button", { onClick: handlers.handleDateApply, style: { padding: '4px 12px', cursor: 'pointer' }, children: "Apply" })] })] }));
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
};
|
|
16
|
+
const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, options = [], isLoadingOptions = false, selectedUser, } = props;
|
|
17
|
+
const state = useColumnHeaderFilterState(getColumnHeaderFilterStateParams(props));
|
|
18
|
+
const { headerRef, isFilterOpen, setFilterOpen, hasActiveFilter, popoverPosition, handlers, } = state;
|
|
45
19
|
return (_jsxs(Box, { ref: headerRef, sx: { display: 'flex', alignItems: 'center', width: '100%', minWidth: 0 }, children: [_jsx(Box, { sx: { flex: 1, minWidth: 0, overflow: 'hidden' }, children: _jsx(Tooltip, { title: columnName, arrow: true, children: _jsx(Typography, { variant: "body2", fontWeight: 600, noWrap: true, "data-header-label": true, sx: { lineHeight: 1.4 }, children: columnName }) }) }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', ml: 0.5, flexShrink: 0 }, children: [onSort && (_jsx(IconButton, { size: "small", onClick: handlers.handleSortClick, "aria-label": `Sort by ${columnName}`, title: isSorted ? (isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort', color: isSorted ? 'primary' : 'default', sx: { p: 0.25 }, children: isSorted ? (isSortedDescending ? (_jsx(ArrowDownwardIcon, { sx: { fontSize: 16 } })) : (_jsx(ArrowUpwardIcon, { sx: { fontSize: 16 } }))) : (_jsx(SwapVertIcon, { sx: { fontSize: 16 } })) })), filterType !== 'none' && (_jsxs(IconButton, { size: "small", onClick: handlers.handleFilterIconClick, "aria-label": `Filter ${columnName}`, title: `Filter ${columnName}`, color: hasActiveFilter || isFilterOpen ? 'primary' : 'default', sx: { p: 0.25, position: 'relative' }, children: [_jsx(FilterListIcon, { sx: { fontSize: 16 } }), hasActiveFilter && (_jsx(Box, { sx: {
|
|
46
20
|
position: 'absolute',
|
|
47
21
|
top: 2,
|
|
@@ -55,6 +29,6 @@ export const ColumnHeaderFilter = React.memo((props) => {
|
|
|
55
29
|
sx: { mt: 0.5, overflow: 'visible' },
|
|
56
30
|
onClick: (e) => e.stopPropagation(),
|
|
57
31
|
},
|
|
58
|
-
}, children: [_jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', px: 1.5, py: 1 }, children: _jsxs(Typography, { variant: "subtitle2", children: ["Filter: ", columnName] }) }),
|
|
32
|
+
}, children: [_jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', px: 1.5, py: 1 }, children: _jsxs(Typography, { variant: "subtitle2", children: ["Filter: ", columnName] }) }), renderFilterContent(filterType, state, options ?? [], isLoadingOptions, selectedUser, materialRenderers)] })] }));
|
|
59
33
|
});
|
|
60
34
|
ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
|
-
import { Box,
|
|
5
|
+
import { Box, Popover, Checkbox, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, } from '@mui/material';
|
|
6
6
|
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
7
7
|
import { ColumnHeaderMenu } from '../ColumnHeaderMenu';
|
|
8
8
|
import { InlineCellEditor } from './InlineCellEditor';
|
|
9
9
|
import { StatusBar } from './StatusBar';
|
|
10
10
|
import { GridContextMenu } from './GridContextMenu';
|
|
11
|
-
import {
|
|
11
|
+
import { EmptyState } from './EmptyState';
|
|
12
|
+
import { LoadingOverlay } from './LoadingOverlay';
|
|
13
|
+
import { DropIndicator } from './DropIndicator';
|
|
14
|
+
import { useDataGridTableOrchestration, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, PREVENT_DEFAULT, NOOP, STOP_PROPAGATION, } from '@alaarab/ogrid-react';
|
|
12
15
|
// ── Module-scope stable styles (avoid per-render Emotion resolutions) ──
|
|
13
16
|
const gridRootSx = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
|
|
14
17
|
// Row
|
|
@@ -48,8 +51,9 @@ const CELL_CONTENT_EDITABLE_SX = { ...CELL_CONTENT_BASE_SX, cursor: 'cell' };
|
|
|
48
51
|
const CELL_CONTENT_NUMERIC_EDITABLE_SX = { ...CELL_CONTENT_NUMERIC_SX, cursor: 'cell' };
|
|
49
52
|
const CELL_CONTENT_BOOLEAN_EDITABLE_SX = { ...CELL_CONTENT_BOOLEAN_SX, cursor: 'cell' };
|
|
50
53
|
// Cell overlay states (only applied to the few active/selected cells)
|
|
51
|
-
|
|
52
|
-
const
|
|
54
|
+
// Active cell: theme-aware bg so dark mode doesn't show white (MUI action.hover adapts to theme)
|
|
55
|
+
const CELL_ACTIVE_SX = { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px', zIndex: 2, position: 'relative', overflow: 'visible', bgcolor: 'action.hover', '&:focus-visible': { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px' } };
|
|
56
|
+
const CELL_IN_RANGE_SX = { bgcolor: 'var(--ogrid-bg-range, rgba(33, 115, 70, 0.12))', '&:focus-visible': { outline: 'none' } };
|
|
53
57
|
const CELL_CUT_RANGE_SX = { bgcolor: 'action.hover', opacity: 0.7 };
|
|
54
58
|
// Pre-computed overlay variant arrays (avoid per-cell array allocation + filter)
|
|
55
59
|
// Key: `${base}_${overlay}` where overlay is 'active' | 'range' | 'cut'
|
|
@@ -105,18 +109,46 @@ const FILL_HANDLE_SX = {
|
|
|
105
109
|
};
|
|
106
110
|
// Cell <td> positioning variants
|
|
107
111
|
const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
|
|
108
|
-
const CELL_TD_PINNED_LEFT_SX = {
|
|
109
|
-
|
|
112
|
+
const CELL_TD_PINNED_LEFT_SX = {
|
|
113
|
+
...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6,
|
|
114
|
+
bgcolor: 'background.paper', willChange: 'transform',
|
|
115
|
+
'&::after': {
|
|
116
|
+
content: '""', position: 'absolute', top: '-1px', right: '-4px', bottom: '-1px',
|
|
117
|
+
width: '4px', background: 'linear-gradient(to right, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const CELL_TD_PINNED_RIGHT_SX = {
|
|
121
|
+
...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6,
|
|
122
|
+
bgcolor: 'background.paper', willChange: 'transform',
|
|
123
|
+
'&::before': {
|
|
124
|
+
content: '""', position: 'absolute', top: '-1px', left: '-4px', bottom: '-1px',
|
|
125
|
+
width: '4px', background: 'linear-gradient(to left, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
110
128
|
// Header cell positioning variants
|
|
111
129
|
const HEADER_BASE_SX = {
|
|
112
130
|
fontWeight: 600,
|
|
113
|
-
position: 'sticky', /*
|
|
131
|
+
position: 'sticky', /* Enables vertical sticky for all headers */
|
|
114
132
|
top: 0, /* Sticky vertically */
|
|
115
133
|
zIndex: 8, /* Stack above body cells */
|
|
116
134
|
bgcolor: 'action.hover' /* Required for sticky overlap */
|
|
117
135
|
};
|
|
118
|
-
const HEADER_PINNED_LEFT_SX = {
|
|
119
|
-
|
|
136
|
+
const HEADER_PINNED_LEFT_SX = {
|
|
137
|
+
...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0,
|
|
138
|
+
zIndex: 10, bgcolor: 'action.hover', willChange: 'transform',
|
|
139
|
+
'&::after': {
|
|
140
|
+
content: '""', position: 'absolute', top: '-1px', right: '-4px', bottom: '-1px',
|
|
141
|
+
width: '4px', background: 'linear-gradient(to right, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const HEADER_PINNED_RIGHT_SX = {
|
|
145
|
+
...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0,
|
|
146
|
+
zIndex: 10, bgcolor: 'action.hover', willChange: 'transform',
|
|
147
|
+
'&::before': {
|
|
148
|
+
content: '""', position: 'absolute', top: '-1px', left: '-4px', bottom: '-1px',
|
|
149
|
+
width: '4px', background: 'linear-gradient(to left, rgba(0,0,0,0.12), transparent)', pointerEvents: 'none',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
120
152
|
// Resize handle
|
|
121
153
|
const RESIZE_HANDLE_SX = {
|
|
122
154
|
position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
|
|
@@ -133,22 +165,9 @@ const WRAPPER_SCROLL_SX = { display: 'flex', flexDirection: 'column', minHeight:
|
|
|
133
165
|
// Table wrapper
|
|
134
166
|
const TABLE_WRAPPER_SX = { position: 'relative', opacity: 1 };
|
|
135
167
|
const TABLE_WRAPPER_LOADING_SX = { position: 'relative', opacity: 0.6 };
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const LOADING_OVERLAY_SX = {
|
|
140
|
-
position: 'absolute', inset: 0, zIndex: 2,
|
|
141
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
142
|
-
background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
|
|
143
|
-
};
|
|
144
|
-
const LOADING_INNER_SX = {
|
|
145
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
|
|
146
|
-
p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
|
|
147
|
-
};
|
|
148
|
-
// Module-scope event handlers
|
|
149
|
-
const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
150
|
-
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
151
|
-
const NOOP = () => { };
|
|
168
|
+
// TableBody — remove bottom border from last row so DataGridTable has no outer border
|
|
169
|
+
// (the OGridLayout container provides the border/radius)
|
|
170
|
+
const TABLE_BODY_SX = { '& tr:last-child td': { borderBottom: 'none' } };
|
|
152
171
|
function GridRowInner(props) {
|
|
153
172
|
const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
|
|
154
173
|
return (_jsxs(TableRow, { selected: isSelected, "data-row-id": rowId, onClick: handleSingleRowClick, sx: ROW_HOVER_SX, children: [hasCheckboxCol && (_jsx(TableCell, { padding: "checkbox", sx: CHECKBOX_CELL_SX, children: _jsx(Box, { "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, sx: CHECKBOX_WRAPPER_SX, children: _jsx(Checkbox, { checked: isSelected, onChange: (_, checked) => handleRowCheckboxChange(rowId, checked, rowIndex, lastMouseShiftRef.current), size: "small", "aria-label": `Select row ${rowIndex + 1}` }) }) })), hasRowNumbersCol && (_jsx(TableCell, { sx: {
|
|
@@ -163,87 +182,37 @@ function GridRowInner(props) {
|
|
|
163
182
|
position: 'sticky',
|
|
164
183
|
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
165
184
|
zIndex: 3,
|
|
166
|
-
}, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: [cl.tdSx, { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }], children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
|
|
185
|
+
}, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { "data-column-id": cl.col.columnId, sx: [cl.tdSx, { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }], children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
|
|
167
186
|
}
|
|
168
187
|
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
169
188
|
function DataGridTableInner(props) {
|
|
170
|
-
const
|
|
171
|
-
const tableContainerRef =
|
|
172
|
-
const state = useDataGridState({ props, wrapperRef });
|
|
173
|
-
const lastMouseShiftRef = useRef(false);
|
|
174
|
-
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
|
|
175
|
-
const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
176
|
-
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
177
|
-
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
178
|
-
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
179
|
-
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
180
|
-
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
181
|
-
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
182
|
-
const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
|
|
183
|
-
// Calculate row number offset for pagination
|
|
184
|
-
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
185
|
-
const fitToContent = layoutMode === 'content';
|
|
186
|
-
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
189
|
+
const o = useDataGridTableOrchestration({ props });
|
|
190
|
+
const { wrapperRef, tableContainerRef, lastMouseShiftRef, interaction, pinning, handleResizeStart, getColumnWidth, isReorderDragging, dropIndicatorX, handleHeaderMouseDown, virtualScrollEnabled, visibleRange, items, getRowId, emptyState, freezeRows, freezeCols, suppressHorizontalScroll, isLoading, loadingMessage, ariaLabel, ariaLabelledBy, columnReorder, density, rowNumberOffset, headerRows, allowOverflowX, fitToContent, editCallbacks, interactionHandlers, cellDescriptorInputRef, pendingEditorValueRef, popoverAnchorElRef, handleSingleRowClick, handlePasteVoid, visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, minTableWidth, columnSizingOverrides, selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, editingCell, setPopoverAnchorEl, cancelPopoverEdit, setActiveCell, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging, menuPosition, closeContextMenu, headerFilterInput, statusBarConfig, showEmptyInGrid, onCellError, } = o;
|
|
187
191
|
// Density-aware cell padding
|
|
188
192
|
const densityPadding = useMemo(() => getDensityPadding(density), [density]);
|
|
189
193
|
const _cellSx = useMemo(() => ({ ...CELL_CONTENT_BASE_SX, ...densityPadding }), [densityPadding]);
|
|
190
194
|
const headerCellSx = useMemo(() => ({ px: densityPadding.px, py: densityPadding.py }), [densityPadding]);
|
|
191
|
-
// Memoize header rows (recursive tree traversal)
|
|
192
|
-
const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
|
|
193
|
-
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
194
|
-
columnSizingOverrides,
|
|
195
|
-
setColumnSizingOverrides,
|
|
196
|
-
});
|
|
197
|
-
const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
|
|
198
|
-
columns: visibleCols,
|
|
199
|
-
columnOrder,
|
|
200
|
-
onColumnOrderChange,
|
|
201
|
-
enabled: columnReorder === true,
|
|
202
|
-
pinnedColumns,
|
|
203
|
-
wrapperRef,
|
|
204
|
-
});
|
|
205
|
-
const virtualScrollEnabled = virtualScroll?.enabled === true;
|
|
206
|
-
const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
|
|
207
|
-
const { visibleRange } = useVirtualScroll({
|
|
208
|
-
totalRows: items.length,
|
|
209
|
-
rowHeight: virtualRowHeight,
|
|
210
|
-
enabled: virtualScrollEnabled,
|
|
211
|
-
overscan: virtualScroll?.overscan,
|
|
212
|
-
containerRef: wrapperRef,
|
|
213
|
-
});
|
|
214
195
|
// Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
|
|
215
196
|
const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
|
|
216
197
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
217
|
-
const isPinnedLeft = col.
|
|
218
|
-
const isPinnedRight = col.
|
|
198
|
+
const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
|
|
199
|
+
const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
|
|
219
200
|
const columnWidth = getColumnWidth(col);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// Stable row-click handler
|
|
231
|
-
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
232
|
-
const handleSingleRowClick = useCallback((e) => {
|
|
233
|
-
if (rowSelection !== 'single')
|
|
234
|
-
return;
|
|
235
|
-
const rowId = e.currentTarget.dataset.rowId;
|
|
236
|
-
if (!rowId)
|
|
237
|
-
return;
|
|
238
|
-
const ids = selectedRowIdsRef.current;
|
|
239
|
-
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
240
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
|
|
241
|
-
}, [rowSelection, updateSelection]);
|
|
201
|
+
const baseTdSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? CELL_TD_PINNED_LEFT_SX : isPinnedRight ? CELL_TD_PINNED_RIGHT_SX : CELL_TD_BASE_SX;
|
|
202
|
+
// Override sticky offset for pinned columns (supports multiple pinned columns)
|
|
203
|
+
const tdSx = isPinnedLeft && pinning.leftOffsets[col.columnId] != null
|
|
204
|
+
? { ...baseTdSx, left: pinning.leftOffsets[col.columnId] }
|
|
205
|
+
: isPinnedRight && pinning.rightOffsets[col.columnId] != null
|
|
206
|
+
? { ...baseTdSx, right: pinning.rightOffsets[col.columnId] }
|
|
207
|
+
: baseTdSx;
|
|
208
|
+
const hasResizeOverride = !!columnSizingOverrides[col.columnId];
|
|
209
|
+
return { col, tdSx, minWidth: hasResizeOverride ? columnWidth : (col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH), width: columnWidth, maxWidth: columnWidth };
|
|
210
|
+
}), [visibleCols, freezeCols, getColumnWidth, columnSizingOverrides, pinning.pinnedColumns, pinning.leftOffsets, pinning.rightOffsets]);
|
|
242
211
|
// Wrapper sx (depends on dynamic values — memoize to avoid recreation)
|
|
243
212
|
const wrapperSx = useMemo(() => ({
|
|
244
213
|
position: 'relative',
|
|
245
214
|
flex: 1,
|
|
246
|
-
minHeight: 0,
|
|
215
|
+
minHeight: isLoading && items.length === 0 ? 200 : 0,
|
|
247
216
|
width: fitToContent ? 'fit-content' : '100%',
|
|
248
217
|
maxWidth: '100%',
|
|
249
218
|
overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
|
|
@@ -252,7 +221,7 @@ function DataGridTableInner(props) {
|
|
|
252
221
|
willChange: 'scroll-position',
|
|
253
222
|
'& [data-drag-range]': { bgcolor: 'rgba(33, 115, 70, 0.12) !important' },
|
|
254
223
|
'& [data-drag-anchor]': { bgcolor: 'background.paper !important' },
|
|
255
|
-
}), [fitToContent, suppressHorizontalScroll, allowOverflowX]);
|
|
224
|
+
}), [fitToContent, suppressHorizontalScroll, allowOverflowX, isLoading, items.length]);
|
|
256
225
|
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
257
226
|
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
|
|
258
227
|
const rowId = getRowId(item);
|
|
@@ -281,7 +250,7 @@ function DataGridTableInner(props) {
|
|
|
281
250
|
},
|
|
282
251
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
283
252
|
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
284
|
-
return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-density": density, sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: {
|
|
253
|
+
return (_jsxs(Box, { sx: gridRootSx, children: [_jsxs(Box, { ref: wrapperRef, tabIndex: 0, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, onKeyDown: handleGridKeyDown, onContextMenu: PREVENT_DEFAULT, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-density": density, sx: wrapperSx, children: [_jsx(Box, { sx: WRAPPER_SCROLL_SX, children: _jsx(TableContainer, { sx: { minWidth: allowOverflowX ? minTableWidth : undefined }, children: _jsxs(Box, { ref: tableContainerRef, sx: isLoading && items.length > 0 ? TABLE_WRAPPER_LOADING_SX : TABLE_WRAPPER_SX, children: [_jsxs(Table, { size: "small", sx: { minWidth: minTableWidth, borderCollapse: 'separate', borderSpacing: 0 }, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, children: [_jsx(TableHead, { sx: STICKY_HEADER_SX, children: headerRows.map((row, rowIdx) => (_jsxs(TableRow, { sx: HEADER_ROW_SX, children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ padding: "checkbox", rowSpan: headerRows.length > 1 ? 1 : undefined, sx: CHECKBOX_CELL_SX }, children: _jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: (_, c) => handleSelectAll(!!c), size: "small", "aria-label": "Select all rows" }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx(TableCell, { ...{ rowSpan: headerRows.length - 1, sx: CHECKBOX_PLACEHOLDER_SX } })), rowIdx === headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
|
|
285
254
|
component: "th",
|
|
286
255
|
scope: "col",
|
|
287
256
|
rowSpan: headerRows.length > 1 ? 1 : undefined,
|
|
@@ -320,10 +289,16 @@ function DataGridTableInner(props) {
|
|
|
320
289
|
const col = cell.columnDef;
|
|
321
290
|
const colIdx = visibleCols.indexOf(col);
|
|
322
291
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
323
|
-
const isPinnedLeft = col.
|
|
324
|
-
const isPinnedRight = col.
|
|
292
|
+
const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
|
|
293
|
+
const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
|
|
325
294
|
const columnWidth = getColumnWidth(col);
|
|
326
|
-
const
|
|
295
|
+
const baseHeaderSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
|
|
296
|
+
// Override sticky offset for pinned columns (supports multiple pinned columns)
|
|
297
|
+
const headerSx = isPinnedLeft && pinning.leftOffsets[col.columnId] != null
|
|
298
|
+
? { ...baseHeaderSx, left: pinning.leftOffsets[col.columnId] }
|
|
299
|
+
: isPinnedRight && pinning.rightOffsets[col.columnId] != null
|
|
300
|
+
? { ...baseHeaderSx, right: pinning.rightOffsets[col.columnId] }
|
|
301
|
+
: baseHeaderSx;
|
|
327
302
|
// Determine aria-sort value for sorted columns
|
|
328
303
|
const isSorted = props.sortBy === col.columnId;
|
|
329
304
|
const ariaSort = isSorted
|
|
@@ -372,8 +347,13 @@ function DataGridTableInner(props) {
|
|
|
372
347
|
'&:hover': {
|
|
373
348
|
bgcolor: 'action.hover',
|
|
374
349
|
},
|
|
375
|
-
}, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) =>
|
|
376
|
-
|
|
350
|
+
}, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) => {
|
|
351
|
+
setActiveCell(null);
|
|
352
|
+
interaction.setSelectionRange(null);
|
|
353
|
+
wrapperRef.current?.focus({ preventScroll: true });
|
|
354
|
+
handleResizeStart(e, col);
|
|
355
|
+
}, sx: RESIZE_HANDLE_SX })] }, col.columnId));
|
|
356
|
+
})] }, rowIdx))) }), !showEmptyInGrid && (_jsxs(TableBody, { sx: TABLE_BODY_SX, children: [virtualScrollEnabled && visibleRange.offsetTop > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetTop }, "aria-hidden": true })), (virtualScrollEnabled
|
|
377
357
|
? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
|
|
378
358
|
const rowIndex = visibleRange.startIndex + i;
|
|
379
359
|
const rowIdStr = getRowId(item);
|
|
@@ -382,17 +362,7 @@ function DataGridTableInner(props) {
|
|
|
382
362
|
: items.map((item, rowIndex) => {
|
|
383
363
|
const rowIdStr = getRowId(item);
|
|
384
364
|
return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), columnLayouts: columnLayouts, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, hasRowNumbersCol: hasRowNumbersCol, rowNumberOffset: rowNumberOffset, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
|
|
385
|
-
})), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(
|
|
386
|
-
|
|
387
|
-
top: 0,
|
|
388
|
-
bottom: 0,
|
|
389
|
-
width: 3,
|
|
390
|
-
bgcolor: 'var(--ogrid-primary, #217346)',
|
|
391
|
-
pointerEvents: 'none',
|
|
392
|
-
zIndex: 100,
|
|
393
|
-
transition: 'left 0.05s',
|
|
394
|
-
left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
|
|
395
|
-
} })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: props.visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: props.columnOrder }), showEmptyInGrid && emptyState && (_jsx(Box, { sx: EMPTY_STATE_SX, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "No results found" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx(Button, { variant: "text", size: "small", onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }))] }) }) }), menuPosition &&
|
|
396
|
-
createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body), _jsx(ColumnHeaderMenu, { columnId: pinning.headerMenu.openForColumn || '', isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable })] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx(Box, { sx: LOADING_OVERLAY_SX, children: _jsxs(Box, { sx: LOADING_INNER_SX, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: loadingMessage })] }) }))] }));
|
|
365
|
+
})), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(DropIndicator, { dropIndicatorX: dropIndicatorX, wrapperLeft: wrapperRef.current?.getBoundingClientRect().left ?? 0 })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset, items: items, visibleColumns: props.visibleColumns, columnSizingOverrides: columnSizingOverrides, columnOrder: props.columnOrder }), showEmptyInGrid && emptyState && (_jsx(EmptyState, { emptyState: emptyState }))] }) }) }), menuPosition &&
|
|
366
|
+
createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: o.interaction.handleSelectAllCells, onClose: closeContextMenu }), document.body), _jsx(ColumnHeaderMenu, { columnId: pinning.headerMenu.openForColumn || '', isOpen: pinning.headerMenu.isOpen, anchorElement: pinning.headerMenu.anchorElement, onClose: pinning.headerMenu.close, onPinLeft: pinning.headerMenu.handlePinLeft, onPinRight: pinning.headerMenu.handlePinRight, onUnpin: pinning.headerMenu.handleUnpin, onSortAsc: pinning.headerMenu.handleSortAsc, onSortDesc: pinning.headerMenu.handleSortDesc, onClearSort: pinning.headerMenu.handleClearSort, onAutosizeThis: pinning.headerMenu.handleAutosizeThis, onAutosizeAll: pinning.headerMenu.handleAutosizeAll, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin, currentSort: pinning.headerMenu.currentSort, isSortable: pinning.headerMenu.isSortable, isResizable: pinning.headerMenu.isResizable })] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx(LoadingOverlay, { message: loadingMessage }))] }));
|
|
397
367
|
}
|
|
398
368
|
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
|
+
export function DropIndicator({ dropIndicatorX, wrapperLeft }) {
|
|
4
|
+
return (_jsx(Box, { sx: {
|
|
5
|
+
position: 'absolute',
|
|
6
|
+
top: 0,
|
|
7
|
+
bottom: 0,
|
|
8
|
+
width: 3,
|
|
9
|
+
bgcolor: 'var(--ogrid-primary, #217346)',
|
|
10
|
+
pointerEvents: 'none',
|
|
11
|
+
zIndex: 100,
|
|
12
|
+
transition: 'left 0.05s',
|
|
13
|
+
left: dropIndicatorX - wrapperLeft,
|
|
14
|
+
} }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Typography, Button } from '@mui/material';
|
|
3
|
+
const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' };
|
|
4
|
+
export function EmptyState({ emptyState }) {
|
|
5
|
+
return (_jsx(Box, { sx: EMPTY_STATE_SX, children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "No results found" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx(Button, { variant: "text", size: "small", onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, CircularProgress, Typography } from '@mui/material';
|
|
3
|
+
const LOADING_OVERLAY_SX = {
|
|
4
|
+
position: 'absolute', inset: 0, zIndex: 2,
|
|
5
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
6
|
+
background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
|
|
7
|
+
};
|
|
8
|
+
const LOADING_INNER_SX = {
|
|
9
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
|
|
10
|
+
p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
|
|
11
|
+
};
|
|
12
|
+
export function LoadingOverlay({ message }) {
|
|
13
|
+
return (_jsx(Box, { sx: LOADING_OVERLAY_SX, children: _jsxs(Box, { sx: LOADING_INNER_SX, children: [_jsx(CircularProgress, { size: 24 }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: message })] }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { Box, useTheme } from '@mui/material';
|
|
5
|
+
import { DataGridTable } from '../DataGridTable/DataGridTable';
|
|
6
|
+
import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
|
|
7
|
+
import { PaginationControls } from '../PaginationControls/PaginationControls';
|
|
8
|
+
import { useOGrid, OGridLayout, } from '@alaarab/ogrid-react';
|
|
9
|
+
const OGridInner = forwardRef(function OGridInner(props, ref) {
|
|
10
|
+
const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
// Set --ogrid-* CSS variables so the shared OGridLayout adapts to MUI theme (both modes)
|
|
13
|
+
const containerSx = {
|
|
14
|
+
display: 'flex', flexDirection: 'column', gap: 1,
|
|
15
|
+
'--ogrid-bg': theme.palette.background.default,
|
|
16
|
+
'--ogrid-border': theme.palette.divider,
|
|
17
|
+
'--ogrid-header-bg': theme.palette.action.hover,
|
|
18
|
+
'--ogrid-fg': theme.palette.text.primary,
|
|
19
|
+
'--ogrid-fg-secondary': theme.palette.text.secondary,
|
|
20
|
+
'--ogrid-fg-muted': theme.palette.text.disabled,
|
|
21
|
+
'--ogrid-hover-bg': theme.palette.action.hover,
|
|
22
|
+
};
|
|
23
|
+
return (_jsx(OGridLayout, { containerComponent: Box, containerProps: { sx: containerSx }, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
|
|
24
|
+
pagination.setPageSize(size);
|
|
25
|
+
pagination.setPage(1);
|
|
26
|
+
}, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
27
|
+
});
|
|
28
|
+
OGridInner.displayName = 'OGrid';
|
|
29
|
+
export const OGrid = React.memo(OGridInner);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OGrid } from './OGrid';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Components
|
|
2
|
-
export { OGrid
|
|
2
|
+
export { OGrid } from './OGrid';
|
|
3
3
|
export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
4
4
|
export { ColumnChooser } from './ColumnChooser/ColumnChooser';
|
|
5
5
|
export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
export type {
|
|
4
|
-
export interface IColumnChooserProps {
|
|
5
|
-
columns: IColumnDefinition[];
|
|
6
|
-
visibleColumns: Set<string>;
|
|
7
|
-
onVisibilityChange: (columnKey: string, visible: boolean) => void;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
2
|
+
import type { IColumnChooserProps } from '@alaarab/ogrid-react';
|
|
3
|
+
export type { IColumnChooserProps };
|
|
10
4
|
export declare const ColumnChooser: React.FC<IColumnChooserProps>;
|
|
@@ -1,22 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
export
|
|
4
|
-
columnKey: string;
|
|
5
|
-
columnName: string;
|
|
6
|
-
filterType: ColumnFilterType;
|
|
7
|
-
isSorted?: boolean;
|
|
8
|
-
isSortedDescending?: boolean;
|
|
9
|
-
onSort?: () => void;
|
|
10
|
-
selectedValues?: string[];
|
|
11
|
-
onFilterChange?: (values: string[]) => void;
|
|
12
|
-
options?: string[];
|
|
13
|
-
isLoadingOptions?: boolean;
|
|
14
|
-
textValue?: string;
|
|
15
|
-
onTextChange?: (value: string) => void;
|
|
16
|
-
selectedUser?: UserLike;
|
|
17
|
-
onUserChange?: (user: UserLike | undefined) => void;
|
|
18
|
-
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
19
|
-
dateValue?: IDateFilterValue;
|
|
20
|
-
onDateChange?: (value: IDateFilterValue | undefined) => void;
|
|
21
|
-
}
|
|
2
|
+
import type { IColumnHeaderFilterProps } from '@alaarab/ogrid-react';
|
|
3
|
+
export type { IColumnHeaderFilterProps };
|
|
22
4
|
export declare const ColumnHeaderFilter: React.FC<IColumnHeaderFilterProps>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface EmptyStateProps {
|
|
3
|
+
emptyState: {
|
|
4
|
+
render?: () => React.ReactNode;
|
|
5
|
+
message?: React.ReactNode;
|
|
6
|
+
hasActiveFilters?: boolean;
|
|
7
|
+
onClearAll?: () => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function EmptyState({ emptyState }: EmptyStateProps): React.ReactElement;
|
|
11
|
+
export {};
|
|
@@ -3,5 +3,3 @@ import { type IOGridProps, type IOGridApi } from '@alaarab/ogrid-react';
|
|
|
3
3
|
export type { IOGridProps } from '@alaarab/ogrid-react';
|
|
4
4
|
declare const OGridInner: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
|
|
5
5
|
export declare const OGrid: typeof OGridInner;
|
|
6
|
-
/** @deprecated Use `OGrid` instead. Backward-compat alias. */
|
|
7
|
-
export declare const MaterialDataTable: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OGrid, type IOGridProps } from './OGrid';
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
pageSize: number;
|
|
5
|
-
totalCount: number;
|
|
6
|
-
onPageChange: (page: number) => void;
|
|
7
|
-
onPageSizeChange: (pageSize: number) => void;
|
|
8
|
-
pageSizeOptions?: number[];
|
|
9
|
-
entityLabelPlural?: string;
|
|
10
|
-
className?: string;
|
|
11
|
-
}
|
|
2
|
+
import type { IPaginationControlsProps } from '@alaarab/ogrid-react';
|
|
3
|
+
export type { IPaginationControlsProps };
|
|
12
4
|
export declare const PaginationControls: React.FC<IPaginationControlsProps>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { OGrid,
|
|
1
|
+
export { OGrid, type IOGridProps } from './OGrid';
|
|
2
2
|
export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
3
3
|
export { ColumnChooser, type IColumnChooserProps } from './ColumnChooser/ColumnChooser';
|
|
4
4
|
export { ColumnHeaderFilter, type IColumnHeaderFilterProps } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-react-material",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "OGrid Material
|
|
3
|
+
"version": "2.0.13",
|
|
4
|
+
"description": "OGrid React Material implementation – MUI Table–based data grid with sorting, filtering, pagination, column chooser, spreadsheet selection, and CSV export.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/types/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=18"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@alaarab/ogrid-react": "2.0.
|
|
42
|
+
"@alaarab/ogrid-react": "2.0.13"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@emotion/react": "^11.0.0",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import * as React from 'react';
|
|
3
|
-
import { forwardRef } from 'react';
|
|
4
|
-
import { Box } from '@mui/material';
|
|
5
|
-
import { DataGridTable } from '../DataGridTable/DataGridTable';
|
|
6
|
-
import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
|
|
7
|
-
import { PaginationControls } from '../PaginationControls/PaginationControls';
|
|
8
|
-
import { useOGrid, OGridLayout, } from '@alaarab/ogrid-react';
|
|
9
|
-
const OGridInner = forwardRef(function OGridInner(props, ref) {
|
|
10
|
-
const { dataGridProps, pagination, columnChooser, layout } = useOGrid(props, ref);
|
|
11
|
-
return (_jsx(OGridLayout, { containerComponent: Box, containerProps: { sx: { display: 'flex', flexDirection: 'column', gap: 1 } }, className: layout.className, sideBar: layout.sideBarProps, toolbar: layout.toolbar, toolbarBelow: layout.toolbarBelow, toolbarEnd: columnChooser.placement === 'toolbar' ? (_jsx(ColumnChooser, { columns: columnChooser.columns, visibleColumns: columnChooser.visibleColumns, onVisibilityChange: columnChooser.onVisibilityChange })) : undefined, pagination: _jsx(PaginationControls, { currentPage: pagination.page, pageSize: pagination.pageSize, totalCount: pagination.displayTotalCount, onPageChange: pagination.setPage, onPageSizeChange: (size) => {
|
|
12
|
-
pagination.setPageSize(size);
|
|
13
|
-
pagination.setPage(1);
|
|
14
|
-
}, pageSizeOptions: pagination.pageSizeOptions, entityLabelPlural: pagination.entityLabelPlural }), children: _jsx(DataGridTable, { ...dataGridProps }) }));
|
|
15
|
-
});
|
|
16
|
-
OGridInner.displayName = 'OGrid';
|
|
17
|
-
export const OGrid = React.memo(OGridInner);
|
|
18
|
-
/** @deprecated Use `OGrid` instead. Backward-compat alias. */
|
|
19
|
-
export const MaterialDataTable = OGrid;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { OGrid, MaterialDataTable } from './MaterialDataTable';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { OGrid, MaterialDataTable, type IOGridProps } from './MaterialDataTable';
|