@alaarab/ogrid-react-material 2.0.9 → 2.0.12
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 +3 -3
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +11 -37
- package/dist/esm/ColumnHeaderMenu/ColumnHeaderMenu.js +48 -34
- package/dist/esm/DataGridTable/DataGridTable.js +153 -140
- 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/PaginationControls/PaginationControls.js +14 -8
- 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/ColumnHeaderMenu/ColumnHeaderMenu.d.ts +10 -2
- 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 +5 -5
- 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">
|
|
@@ -26,7 +26,7 @@ export const ColumnChooser = (props) => {
|
|
|
26
26
|
ev.stopPropagation();
|
|
27
27
|
setColumnVisible(columnKey)(ev.target.checked);
|
|
28
28
|
};
|
|
29
|
-
return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
|
|
29
|
+
return (_jsxs(Box, { className: className, sx: { display: 'inline-flex' }, children: [_jsxs(Button, { ref: buttonRef, variant: "outlined", size: "small", color: "inherit", startIcon: _jsx(ViewColumnIcon, {}), endIcon: isOpen ? _jsx(ExpandLessIcon, {}) : _jsx(ExpandMoreIcon, {}), onClick: handleToggle, "aria-expanded": isOpen, "aria-haspopup": "listbox", sx: {
|
|
30
30
|
textTransform: 'none',
|
|
31
31
|
fontWeight: 600,
|
|
32
32
|
borderColor: isOpen ? 'primary.main' : 'divider',
|
|
@@ -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,39 +1,53 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import Menu from '@mui/material
|
|
4
|
-
import
|
|
5
|
-
import IconButton from '@mui/material/IconButton';
|
|
6
|
-
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
7
|
-
import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-core';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, useEffect, useState } from 'react';
|
|
3
|
+
import { Menu, MenuItem, Divider } from '@mui/material';
|
|
4
|
+
import { getColumnHeaderMenuItems } from '@alaarab/ogrid-core';
|
|
8
5
|
/**
|
|
9
|
-
* Column header dropdown menu for pin/
|
|
10
|
-
* Uses Material UI Menu component.
|
|
6
|
+
* Column header dropdown menu for pin/sort/autosize actions.
|
|
7
|
+
* Uses Material UI Menu component with anchor position.
|
|
11
8
|
*/
|
|
12
9
|
export function ColumnHeaderMenu(props) {
|
|
13
|
-
const {
|
|
14
|
-
const [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const { isOpen, anchorElement, onClose, onPinLeft, onPinRight, onUnpin, onSortAsc, onSortDesc, onClearSort, onAutosizeThis, onAutosizeAll, canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable, } = props;
|
|
11
|
+
const [anchorPosition, setAnchorPosition] = useState(undefined);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (isOpen && anchorElement) {
|
|
14
|
+
const rect = anchorElement.getBoundingClientRect();
|
|
15
|
+
setAnchorPosition({
|
|
16
|
+
top: rect.bottom + 4,
|
|
17
|
+
left: rect.left,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
setAnchorPosition(undefined);
|
|
22
|
+
}
|
|
23
|
+
}, [isOpen, anchorElement]);
|
|
24
|
+
const menuInput = useMemo(() => ({
|
|
25
|
+
canPinLeft,
|
|
26
|
+
canPinRight,
|
|
27
|
+
canUnpin,
|
|
28
|
+
currentSort,
|
|
29
|
+
isSortable,
|
|
30
|
+
isResizable,
|
|
31
|
+
}), [canPinLeft, canPinRight, canUnpin, currentSort, isSortable, isResizable]);
|
|
32
|
+
const items = useMemo(() => getColumnHeaderMenuItems(menuInput), [menuInput]);
|
|
33
|
+
const handlers = {
|
|
34
|
+
pinLeft: onPinLeft,
|
|
35
|
+
pinRight: onPinRight,
|
|
36
|
+
unpin: onUnpin,
|
|
37
|
+
sortAsc: onSortAsc,
|
|
38
|
+
sortDesc: onSortDesc,
|
|
39
|
+
clearSort: onClearSort,
|
|
40
|
+
autosizeThis: onAutosizeThis,
|
|
41
|
+
autosizeAll: onAutosizeAll,
|
|
18
42
|
};
|
|
19
|
-
return (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
}, children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsxs(Menu, { anchorEl: triggerEl, open: Boolean(triggerEl), onClose: () => {
|
|
30
|
-
setTriggerEl(null);
|
|
31
|
-
onClose();
|
|
32
|
-
}, anchorOrigin: {
|
|
33
|
-
vertical: 'bottom',
|
|
34
|
-
horizontal: 'left',
|
|
35
|
-
}, transformOrigin: {
|
|
36
|
-
vertical: 'top',
|
|
37
|
-
horizontal: 'left',
|
|
38
|
-
}, children: [_jsx(MenuItem, { disabled: !canPinLeft, onClick: onPinLeft, children: COLUMN_HEADER_MENU_ITEMS[0].label }), _jsx(MenuItem, { disabled: !canPinRight, onClick: onPinRight, children: COLUMN_HEADER_MENU_ITEMS[1].label }), _jsx(MenuItem, { disabled: !canUnpin, onClick: onUnpin, children: COLUMN_HEADER_MENU_ITEMS[2].label })] })] }));
|
|
43
|
+
return (_jsx(Menu, { open: isOpen && !!anchorPosition, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: anchorPosition, slotProps: {
|
|
44
|
+
paper: {
|
|
45
|
+
sx: {
|
|
46
|
+
minWidth: 140,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}, children: items.map((item, idx) => (_jsxs(React.Fragment, { children: [_jsx(MenuItem, { disabled: item.disabled, onClick: () => {
|
|
50
|
+
handlers[item.id]();
|
|
51
|
+
onClose();
|
|
52
|
+
}, children: item.label }), item.divider && idx < items.length - 1 && _jsx(Divider, {})] }, item.id))) }));
|
|
39
53
|
}
|
|
@@ -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
|
|
@@ -18,7 +21,13 @@ const CHECKBOX_CELL_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLU
|
|
|
18
21
|
const CHECKBOX_WRAPPER_SX = { display: 'flex', alignItems: 'center', justifyContent: 'center' };
|
|
19
22
|
const CHECKBOX_PLACEHOLDER_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH, p: 0 };
|
|
20
23
|
// Header
|
|
21
|
-
const STICKY_HEADER_SX = {
|
|
24
|
+
const STICKY_HEADER_SX = {
|
|
25
|
+
/* Removed position: 'sticky', top: 0 - breaks horizontal sticky on pinned columns.
|
|
26
|
+
Instead, apply sticky to individual header cells (HEADER_BASE_SX). */
|
|
27
|
+
zIndex: 8,
|
|
28
|
+
bgcolor: 'action.hover',
|
|
29
|
+
'& th': { bgcolor: 'action.hover' }
|
|
30
|
+
};
|
|
22
31
|
const HEADER_ROW_SX = { bgcolor: 'action.hover' };
|
|
23
32
|
const GROUP_HEADER_CELL_SX = { textAlign: 'center', fontWeight: 600, borderBottom: 2, borderColor: 'divider', py: 0.75 };
|
|
24
33
|
// Density padding helper
|
|
@@ -34,6 +43,7 @@ const CELL_CONTENT_BASE_SX = {
|
|
|
34
43
|
width: '100%', height: '100%', display: 'flex', alignItems: 'center', minWidth: 0,
|
|
35
44
|
px: '10px', py: '6px', boxSizing: 'border-box', overflow: 'hidden',
|
|
36
45
|
textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none', outline: 'none',
|
|
46
|
+
'&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: '-2px', zIndex: 3 },
|
|
37
47
|
};
|
|
38
48
|
const CELL_CONTENT_NUMERIC_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'flex-end', textAlign: 'right' };
|
|
39
49
|
const CELL_CONTENT_BOOLEAN_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'center', textAlign: 'center' };
|
|
@@ -41,8 +51,9 @@ const CELL_CONTENT_EDITABLE_SX = { ...CELL_CONTENT_BASE_SX, cursor: 'cell' };
|
|
|
41
51
|
const CELL_CONTENT_NUMERIC_EDITABLE_SX = { ...CELL_CONTENT_NUMERIC_SX, cursor: 'cell' };
|
|
42
52
|
const CELL_CONTENT_BOOLEAN_EDITABLE_SX = { ...CELL_CONTENT_BOOLEAN_SX, cursor: 'cell' };
|
|
43
53
|
// Cell overlay states (only applied to the few active/selected cells)
|
|
44
|
-
|
|
45
|
-
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' } };
|
|
46
57
|
const CELL_CUT_RANGE_SX = { bgcolor: 'action.hover', opacity: 0.7 };
|
|
47
58
|
// Pre-computed overlay variant arrays (avoid per-cell array allocation + filter)
|
|
48
59
|
// Key: `${base}_${overlay}` where overlay is 'active' | 'range' | 'cut'
|
|
@@ -98,12 +109,46 @@ const FILL_HANDLE_SX = {
|
|
|
98
109
|
};
|
|
99
110
|
// Cell <td> positioning variants
|
|
100
111
|
const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
|
|
101
|
-
const CELL_TD_PINNED_LEFT_SX = {
|
|
102
|
-
|
|
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
|
+
};
|
|
103
128
|
// Header cell positioning variants
|
|
104
|
-
const HEADER_BASE_SX = {
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
const HEADER_BASE_SX = {
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
position: 'sticky', /* Enables vertical sticky for all headers */
|
|
132
|
+
top: 0, /* Sticky vertically */
|
|
133
|
+
zIndex: 8, /* Stack above body cells */
|
|
134
|
+
bgcolor: 'action.hover' /* Required for sticky overlap */
|
|
135
|
+
};
|
|
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
|
+
};
|
|
107
152
|
// Resize handle
|
|
108
153
|
const RESIZE_HANDLE_SX = {
|
|
109
154
|
position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
|
|
@@ -120,22 +165,9 @@ const WRAPPER_SCROLL_SX = { display: 'flex', flexDirection: 'column', minHeight:
|
|
|
120
165
|
// Table wrapper
|
|
121
166
|
const TABLE_WRAPPER_SX = { position: 'relative', opacity: 1 };
|
|
122
167
|
const TABLE_WRAPPER_LOADING_SX = { position: 'relative', opacity: 0.6 };
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const LOADING_OVERLAY_SX = {
|
|
127
|
-
position: 'absolute', inset: 0, zIndex: 2,
|
|
128
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
129
|
-
background: 'var(--ogrid-loading-bg, rgba(255,255,255,0.7))',
|
|
130
|
-
};
|
|
131
|
-
const LOADING_INNER_SX = {
|
|
132
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
|
|
133
|
-
p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
|
|
134
|
-
};
|
|
135
|
-
// Module-scope event handlers
|
|
136
|
-
const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
137
|
-
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
138
|
-
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' } };
|
|
139
171
|
function GridRowInner(props) {
|
|
140
172
|
const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, hasRowNumbersCol, rowNumberOffset, } = props;
|
|
141
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: {
|
|
@@ -150,82 +182,32 @@ function GridRowInner(props) {
|
|
|
150
182
|
position: 'sticky',
|
|
151
183
|
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
152
184
|
zIndex: 3,
|
|
153
|
-
}, children: rowNumberOffset + rowIndex + 1 })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: cl.tdSx,
|
|
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)))] }));
|
|
154
186
|
}
|
|
155
187
|
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
156
188
|
function DataGridTableInner(props) {
|
|
157
|
-
const
|
|
158
|
-
const tableContainerRef =
|
|
159
|
-
const state = useDataGridState({ props, wrapperRef });
|
|
160
|
-
const lastMouseShiftRef = useRef(false);
|
|
161
|
-
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
|
|
162
|
-
const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
163
|
-
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
164
|
-
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
165
|
-
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
166
|
-
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
167
|
-
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
168
|
-
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
169
|
-
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;
|
|
170
|
-
// Calculate row number offset for pagination
|
|
171
|
-
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
172
|
-
const fitToContent = layoutMode === 'content';
|
|
173
|
-
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;
|
|
174
191
|
// Density-aware cell padding
|
|
175
192
|
const densityPadding = useMemo(() => getDensityPadding(density), [density]);
|
|
176
193
|
const _cellSx = useMemo(() => ({ ...CELL_CONTENT_BASE_SX, ...densityPadding }), [densityPadding]);
|
|
177
194
|
const headerCellSx = useMemo(() => ({ px: densityPadding.px, py: densityPadding.py }), [densityPadding]);
|
|
178
|
-
// Memoize header rows (recursive tree traversal)
|
|
179
|
-
const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
|
|
180
|
-
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
181
|
-
columnSizingOverrides,
|
|
182
|
-
setColumnSizingOverrides,
|
|
183
|
-
});
|
|
184
|
-
const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
|
|
185
|
-
columns: visibleCols,
|
|
186
|
-
columnOrder,
|
|
187
|
-
onColumnOrderChange,
|
|
188
|
-
enabled: columnReorder === true,
|
|
189
|
-
pinnedColumns,
|
|
190
|
-
wrapperRef,
|
|
191
|
-
});
|
|
192
|
-
const virtualScrollEnabled = virtualScroll?.enabled === true;
|
|
193
|
-
const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
|
|
194
|
-
const { visibleRange } = useVirtualScroll({
|
|
195
|
-
totalRows: items.length,
|
|
196
|
-
rowHeight: virtualRowHeight,
|
|
197
|
-
enabled: virtualScrollEnabled,
|
|
198
|
-
overscan: virtualScroll?.overscan,
|
|
199
|
-
containerRef: wrapperRef,
|
|
200
|
-
});
|
|
201
195
|
// Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
|
|
202
196
|
const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
|
|
203
197
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
204
|
-
const isPinnedLeft = col.
|
|
205
|
-
const isPinnedRight = col.
|
|
198
|
+
const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
|
|
199
|
+
const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
|
|
206
200
|
const columnWidth = getColumnWidth(col);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Stable row-click handler
|
|
218
|
-
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
219
|
-
const handleSingleRowClick = useCallback((e) => {
|
|
220
|
-
if (rowSelection !== 'single')
|
|
221
|
-
return;
|
|
222
|
-
const rowId = e.currentTarget.dataset.rowId;
|
|
223
|
-
if (!rowId)
|
|
224
|
-
return;
|
|
225
|
-
const ids = selectedRowIdsRef.current;
|
|
226
|
-
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
227
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
|
|
228
|
-
}, [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]);
|
|
229
211
|
// Wrapper sx (depends on dynamic values — memoize to avoid recreation)
|
|
230
212
|
const wrapperSx = useMemo(() => ({
|
|
231
213
|
position: 'relative',
|
|
@@ -268,42 +250,82 @@ function DataGridTableInner(props) {
|
|
|
268
250
|
},
|
|
269
251
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
270
252
|
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
271
|
-
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: {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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, { ...{
|
|
254
|
+
component: "th",
|
|
255
|
+
scope: "col",
|
|
256
|
+
rowSpan: headerRows.length > 1 ? 1 : undefined,
|
|
257
|
+
sx: {
|
|
258
|
+
width: ROW_NUMBER_COLUMN_WIDTH,
|
|
259
|
+
minWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
260
|
+
maxWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
261
|
+
textAlign: 'center',
|
|
262
|
+
fontWeight: 600,
|
|
263
|
+
backgroundColor: 'action.hover',
|
|
264
|
+
position: 'sticky',
|
|
265
|
+
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
266
|
+
zIndex: 4,
|
|
267
|
+
...headerCellSx,
|
|
268
|
+
}
|
|
269
|
+
}, children: "#" })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol && (_jsx(TableCell, { ...{
|
|
270
|
+
rowSpan: headerRows.length - 1,
|
|
271
|
+
sx: {
|
|
272
|
+
width: ROW_NUMBER_COLUMN_WIDTH,
|
|
273
|
+
minWidth: ROW_NUMBER_COLUMN_WIDTH,
|
|
274
|
+
position: 'sticky',
|
|
275
|
+
left: hasCheckboxCol ? CHECKBOX_COLUMN_WIDTH : 0,
|
|
276
|
+
zIndex: 4,
|
|
277
|
+
backgroundColor: 'background.paper',
|
|
278
|
+
}
|
|
289
279
|
} })), row.map((cell, cellIdx) => {
|
|
290
280
|
if (cell.isGroup) {
|
|
291
|
-
return (_jsx(TableCell, {
|
|
281
|
+
return (_jsx(TableCell, { ...{
|
|
282
|
+
colSpan: cell.colSpan,
|
|
283
|
+
component: "th",
|
|
284
|
+
scope: "colgroup",
|
|
285
|
+
sx: GROUP_HEADER_CELL_SX
|
|
286
|
+
}, children: cell.label }, cellIdx));
|
|
292
287
|
}
|
|
293
288
|
// Leaf cell
|
|
294
289
|
const col = cell.columnDef;
|
|
295
290
|
const colIdx = visibleCols.indexOf(col);
|
|
296
291
|
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
297
|
-
const isPinnedLeft = col.
|
|
298
|
-
const isPinnedRight = col.
|
|
292
|
+
const isPinnedLeft = pinning.pinnedColumns[col.columnId] === 'left';
|
|
293
|
+
const isPinnedRight = pinning.pinnedColumns[col.columnId] === 'right';
|
|
299
294
|
const columnWidth = getColumnWidth(col);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
|
302
|
+
// Determine aria-sort value for sorted columns
|
|
303
|
+
const isSorted = props.sortBy === col.columnId;
|
|
304
|
+
const ariaSort = isSorted
|
|
305
|
+
? (props.sortDirection === 'asc' ? 'ascending' : 'descending')
|
|
306
|
+
: undefined;
|
|
307
|
+
return (_jsxs(TableCell, { ...{
|
|
308
|
+
component: "th",
|
|
309
|
+
scope: "col",
|
|
310
|
+
'data-column-id': col.columnId,
|
|
311
|
+
rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
|
|
312
|
+
'aria-sort': ariaSort,
|
|
313
|
+
sx: {
|
|
314
|
+
...headerSx,
|
|
315
|
+
...headerCellSx,
|
|
316
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
317
|
+
width: columnWidth,
|
|
318
|
+
maxWidth: columnWidth,
|
|
319
|
+
...(columnReorder ? { cursor: isReorderDragging ? 'grabbing' : 'grab' } : {}),
|
|
320
|
+
'&:focus-visible': {
|
|
321
|
+
outline: '2px solid',
|
|
322
|
+
outlineColor: 'primary.main',
|
|
323
|
+
outlineOffset: '-2px',
|
|
324
|
+
zIndex: 11,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
onMouseDown: columnReorder ? (e) => handleHeaderMouseDown(col.columnId, e) : undefined
|
|
328
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { component: "button", onClick: (e) => {
|
|
307
329
|
e.stopPropagation();
|
|
308
330
|
pinning.headerMenu.open(col.columnId, e.currentTarget);
|
|
309
331
|
}, "aria-label": "Column options", title: "Column options", sx: {
|
|
@@ -314,8 +336,8 @@ function DataGridTableInner(props) {
|
|
|
314
336
|
fontSize: '16px',
|
|
315
337
|
lineHeight: 1,
|
|
316
338
|
color: 'text.secondary',
|
|
317
|
-
opacity:
|
|
318
|
-
transition: '
|
|
339
|
+
opacity: 1,
|
|
340
|
+
transition: 'background-color 0.15s',
|
|
319
341
|
borderRadius: '3px',
|
|
320
342
|
display: 'flex',
|
|
321
343
|
alignItems: 'center',
|
|
@@ -324,13 +346,14 @@ function DataGridTableInner(props) {
|
|
|
324
346
|
height: '20px',
|
|
325
347
|
'&:hover': {
|
|
326
348
|
bgcolor: 'action.hover',
|
|
327
|
-
opacity: 1,
|
|
328
|
-
},
|
|
329
|
-
'th:hover &': {
|
|
330
|
-
opacity: 1,
|
|
331
349
|
},
|
|
332
|
-
}, children: "\u22EE" })] }), _jsx(Box, { onMouseDown: (e) =>
|
|
333
|
-
|
|
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
|
|
334
357
|
? items.slice(visibleRange.startIndex, visibleRange.endIndex + 1).map((item, i) => {
|
|
335
358
|
const rowIndex = visibleRange.startIndex + i;
|
|
336
359
|
const rowIdStr = getRowId(item);
|
|
@@ -339,17 +362,7 @@ function DataGridTableInner(props) {
|
|
|
339
362
|
: items.map((item, rowIndex) => {
|
|
340
363
|
const rowIdStr = getRowId(item);
|
|
341
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));
|
|
342
|
-
})), virtualScrollEnabled && visibleRange.offsetBottom > 0 && (_jsx(TableRow, { style: { height: visibleRange.offsetBottom }, "aria-hidden": true }))] }))] }), isReorderDragging && dropIndicatorX != null && (_jsx(
|
|
343
|
-
|
|
344
|
-
top: 0,
|
|
345
|
-
bottom: 0,
|
|
346
|
-
width: 3,
|
|
347
|
-
bgcolor: 'var(--ogrid-primary, #217346)',
|
|
348
|
-
pointerEvents: 'none',
|
|
349
|
-
zIndex: 100,
|
|
350
|
-
transition: 'left 0.05s',
|
|
351
|
-
left: dropIndicatorX - (wrapperRef.current?.getBoundingClientRect().left ?? 0),
|
|
352
|
-
} })), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), 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 &&
|
|
353
|
-
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, canPinLeft: pinning.headerMenu.canPinLeft, canPinRight: pinning.headerMenu.canPinRight, canUnpin: pinning.headerMenu.canUnpin })] }), 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 }))] }));
|
|
354
367
|
}
|
|
355
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';
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { useMemo, useCallback } from 'react';
|
|
4
3
|
import { IconButton, Button, Select, MenuItem, Box, Typography, } from '@mui/material';
|
|
5
4
|
import { FirstPage as FirstPageIcon, LastPage as LastPageIcon, ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, } from '@mui/icons-material';
|
|
6
|
-
import {
|
|
5
|
+
import { usePaginationControls } from '@alaarab/ogrid-react';
|
|
7
6
|
export const PaginationControls = React.memo((props) => {
|
|
8
7
|
const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className, } = props;
|
|
9
|
-
const labelPlural
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const { labelPlural, vm, handlePageSizeChange } = usePaginationControls({
|
|
9
|
+
currentPage,
|
|
10
|
+
pageSize,
|
|
11
|
+
totalCount,
|
|
12
|
+
onPageChange,
|
|
13
|
+
onPageSizeChange,
|
|
14
|
+
pageSizeOptions,
|
|
15
|
+
entityLabelPlural,
|
|
16
|
+
});
|
|
17
|
+
const handlePageSizeChangeEvent = (event) => {
|
|
18
|
+
handlePageSizeChange(Number(event.target.value));
|
|
19
|
+
};
|
|
14
20
|
if (!vm) {
|
|
15
21
|
return null;
|
|
16
22
|
}
|
|
@@ -25,5 +31,5 @@ export const PaginationControls = React.memo((props) => {
|
|
|
25
31
|
width: '100%',
|
|
26
32
|
minWidth: 0,
|
|
27
33
|
boxSizing: 'border-box',
|
|
28
|
-
}, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange:
|
|
34
|
+
}, children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Showing ", startItem, " to ", endItem, " of ", totalCount.toLocaleString(), " ", labelPlural] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(IconButton, { size: "small", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "First page", children: _jsx(FirstPageIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Previous page", children: _jsx(ChevronLeftIcon, { fontSize: "small" }) }), showStartEllipsis && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(1), "aria-label": "Page 1", sx: { minWidth: 32, px: 0.5 }, children: "1" }), _jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" })] })), pageNumbers.map((pageNum) => (_jsx(Button, { variant: currentPage === pageNum ? 'contained' : 'outlined', size: "small", onClick: () => onPageChange(pageNum), "aria-label": `Page ${pageNum}`, "aria-current": currentPage === pageNum ? 'page' : undefined, sx: { minWidth: 32, px: 0.5 }, children: pageNum }, pageNum))), showEndEllipsis && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mx: 0.5 }, "aria-hidden": true, children: "\u2026" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => onPageChange(totalPages), "aria-label": `Page ${totalPages}`, sx: { minWidth: 32, px: 0.5 }, children: totalPages })] })), _jsx(IconButton, { size: "small", onClick: () => onPageChange(currentPage + 1), disabled: currentPage >= totalPages, "aria-label": "Next page", children: _jsx(ChevronRightIcon, { fontSize: "small" }) }), _jsx(IconButton, { size: "small", onClick: () => onPageChange(totalPages), disabled: currentPage >= totalPages, "aria-label": "Last page", children: _jsx(LastPageIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Rows" }), _jsx(Select, { value: pageSize, onChange: handlePageSizeChangeEvent, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
|
|
29
35
|
});
|
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>;
|
|
@@ -6,12 +6,20 @@ export interface ColumnHeaderMenuProps {
|
|
|
6
6
|
onPinLeft: () => void;
|
|
7
7
|
onPinRight: () => void;
|
|
8
8
|
onUnpin: () => void;
|
|
9
|
+
onSortAsc: () => void;
|
|
10
|
+
onSortDesc: () => void;
|
|
11
|
+
onClearSort: () => void;
|
|
12
|
+
onAutosizeThis: () => void;
|
|
13
|
+
onAutosizeAll: () => void;
|
|
9
14
|
canPinLeft: boolean;
|
|
10
15
|
canPinRight: boolean;
|
|
11
16
|
canUnpin: boolean;
|
|
17
|
+
currentSort: 'asc' | 'desc' | null;
|
|
18
|
+
isSortable: boolean;
|
|
19
|
+
isResizable: boolean;
|
|
12
20
|
}
|
|
13
21
|
/**
|
|
14
|
-
* Column header dropdown menu for pin/
|
|
15
|
-
* Uses Material UI Menu component.
|
|
22
|
+
* Column header dropdown menu for pin/sort/autosize actions.
|
|
23
|
+
* Uses Material UI Menu component with anchor position.
|
|
16
24
|
*/
|
|
17
25
|
export declare function ColumnHeaderMenu(props: ColumnHeaderMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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.12",
|
|
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.12"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@emotion/react": "^11.0.0",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"@testing-library/jest-dom": "^6.9.1",
|
|
59
59
|
"@testing-library/react": "^16.3.2",
|
|
60
60
|
"@testing-library/user-event": "^14.6.1",
|
|
61
|
-
"@types/react": "^
|
|
62
|
-
"@types/react-dom": "^
|
|
61
|
+
"@types/react": "^19.0.0",
|
|
62
|
+
"@types/react-dom": "^19.0.0",
|
|
63
63
|
"eslint-plugin-storybook": "10.2.8",
|
|
64
64
|
"react": "^18.3.1",
|
|
65
65
|
"react-dom": "^18.3.1",
|
|
@@ -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';
|