@alaarab/ogrid-react-material 2.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/esm/ColumnChooser/ColumnChooser.js +53 -0
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +60 -0
- package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +9 -0
- package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +17 -0
- package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +15 -0
- package/dist/esm/ColumnHeaderFilter/index.js +1 -0
- package/dist/esm/DataGridTable/DataGridTable.js +243 -0
- package/dist/esm/DataGridTable/GridContextMenu.js +18 -0
- package/dist/esm/DataGridTable/InlineCellEditor.js +6 -0
- package/dist/esm/DataGridTable/StatusBar.js +13 -0
- package/dist/esm/MaterialDataTable/MaterialDataTable.js +19 -0
- package/dist/esm/MaterialDataTable/index.js +1 -0
- package/dist/esm/PaginationControls/PaginationControls.js +29 -0
- package/dist/esm/index.js +8 -0
- package/dist/types/ColumnChooser/ColumnChooser.d.ts +10 -0
- package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +22 -0
- package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +14 -0
- package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +13 -0
- package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +8 -0
- package/dist/types/ColumnHeaderFilter/index.d.ts +1 -0
- package/dist/types/DataGridTable/DataGridTable.d.ts +5 -0
- package/dist/types/DataGridTable/GridContextMenu.d.ts +10 -0
- package/dist/types/DataGridTable/InlineCellEditor.d.ts +12 -0
- package/dist/types/DataGridTable/StatusBar.d.ts +16 -0
- package/dist/types/MaterialDataTable/MaterialDataTable.d.ts +7 -0
- package/dist/types/MaterialDataTable/index.d.ts +1 -0
- package/dist/types/PaginationControls/PaginationControls.d.ts +12 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>OGrid for Material UI</strong> — The lightweight React data grid with enterprise features and zero enterprise cost.
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@alaarab/ogrid-react-material"><img src="https://img.shields.io/npm/v/@alaarab/ogrid-react-material?color=%23217346&label=npm" alt="npm version" /></a>
|
|
7
|
+
<a href="https://github.com/alaarab/ogrid/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" /></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/React-17%20%7C%2018%20%7C%2019-blue" alt="React 17, 18, 19" />
|
|
9
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict" />
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://alaarab.github.io/ogrid/">Documentation</a> · <a href="https://alaarab.github.io/ogrid/docs/getting-started/overview">Getting Started</a> · <a href="https://alaarab.github.io/ogrid/docs/api/ogrid-props">API Reference</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
OGrid data grid for **[Material UI v7](https://mui.com/)**, built on MUI Table. Also available for [Radix UI](https://www.npmjs.com/package/@alaarab/ogrid-react-radix) (default, lightweight) and [Fluent UI](https://www.npmjs.com/package/@alaarab/ogrid-react-fluent). Same API, just swap the import.
|
|
19
|
+
|
|
20
|
+
## Why OGrid?
|
|
21
|
+
|
|
22
|
+
| | OGrid | AG Grid Community | AG Grid Enterprise |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| Spreadsheet selection | Built-in | - | $999/dev/year |
|
|
25
|
+
| Clipboard (copy/paste) | Built-in | - | $999/dev/year |
|
|
26
|
+
| Fill handle | Built-in | - | $999/dev/year |
|
|
27
|
+
| Undo/redo | Built-in | - | $999/dev/year |
|
|
28
|
+
| Context menu | Built-in | - | $999/dev/year |
|
|
29
|
+
| Status bar | Built-in | - | $999/dev/year |
|
|
30
|
+
| Side bar | Built-in | - | $999/dev/year |
|
|
31
|
+
| Cell editing | Built-in | Built-in | Built-in |
|
|
32
|
+
| Sorting & filtering | Built-in | Built-in | Built-in |
|
|
33
|
+
| **License** | **MIT (free)** | MIT | Commercial |
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
Sorting · Filtering (text, multi-select, date range, people picker) · Pagination · Cell editing (inline, select, checkbox, rich select, date, custom popover) · Spreadsheet selection · Clipboard · Fill handle · Undo/redo · Row selection · Column groups · Column pinning · Column resize · Column chooser · Side bar · Context menu · Status bar with aggregations · CSV export · Grid API · Server-side data · Column state persistence · Keyboard navigation (Excel-style Ctrl+Arrow) · Built-in column types (text, numeric, date, boolean) · React 17/18/19 · TypeScript strict
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @alaarab/ogrid-react-material
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Peer Dependencies
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
@mui/material ^7.0.0
|
|
49
|
+
@mui/icons-material ^7.0.0
|
|
50
|
+
@emotion/react ^11.0.0
|
|
51
|
+
@emotion/styled ^11.0.0
|
|
52
|
+
react ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
53
|
+
react-dom ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { OGrid, type IColumnDef } from '@alaarab/ogrid-react-material';
|
|
60
|
+
|
|
61
|
+
const columns: IColumnDef<Employee>[] = [
|
|
62
|
+
{ columnId: 'name', name: 'Name', sortable: true, editable: true },
|
|
63
|
+
{ columnId: 'department', name: 'Department',
|
|
64
|
+
filterable: { type: 'multiSelect' },
|
|
65
|
+
cellEditor: 'richSelect', cellEditorParams: { values: ['Engineering', 'Sales', 'Marketing'] } },
|
|
66
|
+
{ columnId: 'salary', name: 'Salary', type: 'numeric', editable: true,
|
|
67
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}` },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
<OGrid
|
|
71
|
+
columns={columns}
|
|
72
|
+
data={employees}
|
|
73
|
+
getRowId={(e) => e.id}
|
|
74
|
+
editable
|
|
75
|
+
cellSelection
|
|
76
|
+
statusBar
|
|
77
|
+
sideBar
|
|
78
|
+
/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
Full docs, API reference, and interactive examples at **[alaarab.github.io/ogrid](https://alaarab.github.io/ogrid/)**.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT — Free forever. No enterprise tiers. No feature paywalls.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
import { Button, Popover, Checkbox, Box, Typography, FormControlLabel, } from '@mui/material';
|
|
4
|
+
import { ViewColumn as ViewColumnIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, } from '@mui/icons-material';
|
|
5
|
+
import { useColumnChooserState } from '@alaarab/ogrid-react';
|
|
6
|
+
export const ColumnChooser = (props) => {
|
|
7
|
+
const { columns, visibleColumns, onVisibilityChange, className } = props;
|
|
8
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
9
|
+
const buttonRef = useRef(null);
|
|
10
|
+
const { open: isOpen, setOpen, handleClose, handleCheckboxChange: setColumnVisible, handleSelectAll, handleClearAll, visibleCount, totalCount, } = useColumnChooserState({ columns, visibleColumns, onVisibilityChange });
|
|
11
|
+
const handleToggle = (e) => {
|
|
12
|
+
if (isOpen) {
|
|
13
|
+
handleClose();
|
|
14
|
+
setAnchorEl(null);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
setAnchorEl(e.currentTarget);
|
|
18
|
+
setOpen(true);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const handlePopoverClose = () => {
|
|
22
|
+
handleClose();
|
|
23
|
+
setAnchorEl(null);
|
|
24
|
+
};
|
|
25
|
+
const handleCheckboxChange = (columnKey) => (ev) => {
|
|
26
|
+
ev.stopPropagation();
|
|
27
|
+
setColumnVisible(columnKey)(ev.target.checked);
|
|
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: {
|
|
30
|
+
textTransform: 'none',
|
|
31
|
+
fontWeight: 600,
|
|
32
|
+
borderColor: isOpen ? 'primary.main' : 'divider',
|
|
33
|
+
}, children: ["Column Visibility (", visibleCount, " of ", totalCount, ")"] }), _jsxs(Popover, { open: isOpen, anchorEl: anchorEl, onClose: handlePopoverClose, anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, slotProps: {
|
|
34
|
+
paper: {
|
|
35
|
+
sx: { mt: 0.5, minWidth: 220 },
|
|
36
|
+
},
|
|
37
|
+
}, children: [_jsx(Box, { sx: {
|
|
38
|
+
px: 1.5,
|
|
39
|
+
py: 1,
|
|
40
|
+
borderBottom: 1,
|
|
41
|
+
borderColor: 'divider',
|
|
42
|
+
bgcolor: 'grey.50',
|
|
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
|
+
display: 'flex',
|
|
45
|
+
justifyContent: 'flex-end',
|
|
46
|
+
gap: 1,
|
|
47
|
+
px: 1.5,
|
|
48
|
+
py: 1,
|
|
49
|
+
borderTop: 1,
|
|
50
|
+
borderColor: 'divider',
|
|
51
|
+
bgcolor: 'grey.50',
|
|
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
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Popover, Tooltip, IconButton, Box, Typography } from '@mui/material';
|
|
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';
|
|
6
|
+
import { TextFilterPopover } from './TextFilterPopover';
|
|
7
|
+
import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
|
|
8
|
+
import { PeopleFilterPopover } from './PeopleFilterPopover';
|
|
9
|
+
export const ColumnHeaderFilter = React.memo((props) => {
|
|
10
|
+
const { columnName, filterType, isSorted = false, isSortedDescending = false, onSort, selectedValues, onFilterChange, options = [], isLoadingOptions = false, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = props;
|
|
11
|
+
const state = useColumnHeaderFilterState({
|
|
12
|
+
filterType,
|
|
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
|
+
};
|
|
45
|
+
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
|
+
position: 'absolute',
|
|
47
|
+
top: 2,
|
|
48
|
+
right: 2,
|
|
49
|
+
width: 6,
|
|
50
|
+
height: 6,
|
|
51
|
+
borderRadius: '50%',
|
|
52
|
+
bgcolor: 'primary.main',
|
|
53
|
+
} }))] }))] }), _jsxs(Popover, { open: isFilterOpen && filterType !== 'none', onClose: () => setFilterOpen(false), anchorReference: "anchorPosition", anchorPosition: popoverPosition ?? { top: 0, left: 0 }, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, slotProps: {
|
|
54
|
+
paper: {
|
|
55
|
+
sx: { mt: 0.5, overflow: 'visible' },
|
|
56
|
+
onClick: (e) => e.stopPropagation(),
|
|
57
|
+
},
|
|
58
|
+
}, children: [_jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', px: 1.5, py: 1 }, children: _jsxs(Typography, { variant: "subtitle2", children: ["Filter: ", columnName] }) }), renderPopoverContent()] })] }));
|
|
59
|
+
});
|
|
60
|
+
ColumnHeaderFilter.displayName = 'ColumnHeaderFilter';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TextField, Checkbox, CircularProgress, Button, Box, Typography, InputAdornment, FormControlLabel, } from '@mui/material';
|
|
3
|
+
import { Search as SearchIcon } from '@mui/icons-material';
|
|
4
|
+
export const MultiSelectFilterPopover = ({ searchText, onSearchChange, options, filteredOptions, selected, onOptionToggle, onSelectAll, onClearSelection, onApply, isLoading, }) => (_jsxs(Box, { sx: { width: 280 }, children: [_jsxs(Box, { sx: { p: 1.5, pb: 0.5 }, children: [_jsx(TextField, { placeholder: "Search...", value: searchText, onChange: (e) => onSearchChange(e.target.value), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", size: "small", fullWidth: true, slotProps: {
|
|
5
|
+
input: {
|
|
6
|
+
startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small" }) })),
|
|
7
|
+
},
|
|
8
|
+
} }), _jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { mt: 0.5, display: 'block' }, children: [filteredOptions.length, " of ", options.length, " options"] })] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', px: 1.5, py: 0.5 }, children: [_jsxs(Button, { size: "small", onClick: onSelectAll, children: ["Select All (", filteredOptions.length, ")"] }), _jsx(Button, { size: "small", onClick: onClearSelection, children: "Clear" })] }), _jsx(Box, { sx: { maxHeight: 240, overflowY: 'auto', px: 0.5 }, children: isLoading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 2 }, children: _jsx(CircularProgress, { size: 24 }) })) : filteredOptions.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { py: 2, textAlign: 'center' }, children: "No options found" })) : (filteredOptions.map((option) => (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: selected.has(option), onChange: (e) => onOptionToggle(option, e.target.checked) }), label: _jsx(Typography, { variant: "body2", children: option }), sx: { display: 'flex', mx: 0, '& .MuiFormControlLabel-label': { flex: 1, minWidth: 0 } } }, option)))) }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, p: 1.5, pt: 1, borderTop: 1, borderColor: 'divider' }, children: [_jsx(Button, { size: "small", onClick: onClearSelection, children: "Clear" }), _jsx(Button, { size: "small", variant: "contained", onClick: onApply, children: "Apply" })] })] }));
|
|
9
|
+
MultiSelectFilterPopover.displayName = 'MultiSelectFilterPopover';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TextField, CircularProgress, Button, Box, Typography, InputAdornment, Avatar, IconButton, } from '@mui/material';
|
|
3
|
+
import { Search as SearchIcon, Clear as ClearIcon } from '@mui/icons-material';
|
|
4
|
+
export const PeopleFilterPopover = ({ selectedUser, searchText, onSearchChange, suggestions, isLoading, onUserSelect, onClearUser, inputRef, }) => (_jsxs(Box, { sx: { width: 300 }, children: [selectedUser && (_jsxs(Box, { sx: { p: 1.5, pb: 1, borderBottom: 1, borderColor: 'divider' }, children: [_jsx(Typography, { variant: "caption", color: "text.secondary", children: "Currently filtered by:" }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }, children: [_jsx(Avatar, { src: selectedUser.photo, alt: selectedUser.displayName, sx: { width: 32, height: 32 }, children: selectedUser.displayName?.[0] }), _jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [_jsx(Typography, { variant: "body2", noWrap: true, children: selectedUser.displayName }), _jsx(Typography, { variant: "caption", color: "text.secondary", noWrap: true, children: selectedUser.email })] }), _jsx(IconButton, { size: "small", onClick: onClearUser, "aria-label": "Remove filter", children: _jsx(ClearIcon, { fontSize: "small" }) })] })] })), _jsx(Box, { sx: { p: 1.5, pb: 0.5 }, children: _jsx(TextField, { inputRef: inputRef, placeholder: "Search for a person...", value: searchText, onChange: (e) => onSearchChange(e.target.value), onKeyDown: (e) => e.stopPropagation(), autoComplete: "off", size: "small", fullWidth: true, slotProps: {
|
|
5
|
+
input: {
|
|
6
|
+
startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small" }) })),
|
|
7
|
+
},
|
|
8
|
+
} }) }), _jsx(Box, { sx: { maxHeight: 240, overflowY: 'auto' }, children: isLoading && searchText.trim() ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 2 }, children: _jsx(CircularProgress, { size: 24 }) })) : suggestions.length === 0 && searchText.trim() ? (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { py: 2, textAlign: 'center' }, children: "No results found" })) : searchText.trim() ? (suggestions.map((user) => (_jsxs(Box, { onClick: () => onUserSelect(user), sx: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
gap: 1,
|
|
12
|
+
px: 1.5,
|
|
13
|
+
py: 1,
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
'&:hover': { bgcolor: 'action.hover' },
|
|
16
|
+
}, children: [_jsx(Avatar, { src: user.photo, alt: user.displayName, sx: { width: 32, height: 32 }, children: user.displayName?.[0] }), _jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [_jsx(Typography, { variant: "body2", noWrap: true, children: user.displayName }), _jsx(Typography, { variant: "caption", color: "text.secondary", noWrap: true, children: user.email })] })] }, user.id || user.email || user.displayName)))) : (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { py: 2, textAlign: 'center' }, children: "Type to search..." })) }), selectedUser && (_jsx(Box, { sx: { p: 1.5, pt: 1, borderTop: 1, borderColor: 'divider' }, children: _jsx(Button, { size: "small", fullWidth: true, onClick: onClearUser, children: "Clear Filter" }) }))] }));
|
|
17
|
+
PeopleFilterPopover.displayName = 'PeopleFilterPopover';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TextField, Button, Box, InputAdornment } from '@mui/material';
|
|
3
|
+
import { Search as SearchIcon } from '@mui/icons-material';
|
|
4
|
+
export const TextFilterPopover = ({ value, onValueChange, onApply, onClear, }) => (_jsxs(Box, { sx: { width: 260 }, children: [_jsx(Box, { sx: { p: 1.5 }, children: _jsx(TextField, { placeholder: "Enter search term...", value: value, onChange: (e) => onValueChange(e.target.value), onKeyDown: (e) => {
|
|
5
|
+
e.stopPropagation();
|
|
6
|
+
if (e.key === 'Enter') {
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
onApply();
|
|
9
|
+
}
|
|
10
|
+
}, autoComplete: "off", size: "small", fullWidth: true, slotProps: {
|
|
11
|
+
input: {
|
|
12
|
+
startAdornment: (_jsx(InputAdornment, { position: "start", children: _jsx(SearchIcon, { fontSize: "small" }) })),
|
|
13
|
+
},
|
|
14
|
+
} }) }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 1, p: 1.5, pt: 0 }, children: [_jsx(Button, { size: "small", disabled: !value, onClick: onClear, children: "Clear" }), _jsx(Button, { size: "small", variant: "contained", onClick: onApply, children: "Apply" })] })] }));
|
|
15
|
+
TextFilterPopover.displayName = 'TextFilterPopover';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnHeaderFilter } from './ColumnHeaderFilter';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Box, CircularProgress, Typography, Button, Popover, Checkbox, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, } from '@mui/material';
|
|
6
|
+
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
7
|
+
import { InlineCellEditor } from './InlineCellEditor';
|
|
8
|
+
import { StatusBar } from './StatusBar';
|
|
9
|
+
import { GridContextMenu } from './GridContextMenu';
|
|
10
|
+
import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, MarchingAntsOverlay, buildHeaderRows, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
|
|
11
|
+
// ── Module-scope stable styles (avoid per-render Emotion resolutions) ──
|
|
12
|
+
const gridRootSx = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
|
|
13
|
+
// Row
|
|
14
|
+
const ROW_HOVER_SX = { '&:hover': { bgcolor: 'action.hover' } };
|
|
15
|
+
// Checkbox column
|
|
16
|
+
const CHECKBOX_CELL_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH, maxWidth: CHECKBOX_COLUMN_WIDTH, textAlign: 'center' };
|
|
17
|
+
const CHECKBOX_WRAPPER_SX = { display: 'flex', alignItems: 'center', justifyContent: 'center' };
|
|
18
|
+
const CHECKBOX_PLACEHOLDER_SX = { width: CHECKBOX_COLUMN_WIDTH, minWidth: CHECKBOX_COLUMN_WIDTH, p: 0 };
|
|
19
|
+
// Header
|
|
20
|
+
const STICKY_HEADER_SX = { position: 'sticky', top: 0, zIndex: 8, bgcolor: 'action.hover', '& th': { bgcolor: 'action.hover' } };
|
|
21
|
+
const HEADER_ROW_SX = { bgcolor: 'action.hover' };
|
|
22
|
+
const GROUP_HEADER_CELL_SX = { textAlign: 'center', fontWeight: 600, borderBottom: 2, borderColor: 'divider', py: 0.75 };
|
|
23
|
+
// Cell content base variants (selected by column type + editability)
|
|
24
|
+
const CELL_CONTENT_BASE_SX = {
|
|
25
|
+
width: '100%', height: '100%', display: 'flex', alignItems: 'center', minWidth: 0,
|
|
26
|
+
px: '10px', py: '6px', boxSizing: 'border-box', overflow: 'hidden',
|
|
27
|
+
textOverflow: 'ellipsis', whiteSpace: 'nowrap', userSelect: 'none', outline: 'none',
|
|
28
|
+
};
|
|
29
|
+
const CELL_CONTENT_NUMERIC_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'flex-end', textAlign: 'right' };
|
|
30
|
+
const CELL_CONTENT_BOOLEAN_SX = { ...CELL_CONTENT_BASE_SX, justifyContent: 'center', textAlign: 'center' };
|
|
31
|
+
const CELL_CONTENT_EDITABLE_SX = { ...CELL_CONTENT_BASE_SX, cursor: 'cell' };
|
|
32
|
+
const CELL_CONTENT_NUMERIC_EDITABLE_SX = { ...CELL_CONTENT_NUMERIC_SX, cursor: 'cell' };
|
|
33
|
+
const CELL_CONTENT_BOOLEAN_EDITABLE_SX = { ...CELL_CONTENT_BOOLEAN_SX, cursor: 'cell' };
|
|
34
|
+
// Cell overlay states (only applied to the few active/selected cells)
|
|
35
|
+
const CELL_ACTIVE_SX = { outline: '2px solid var(--ogrid-selection, #217346)', outlineOffset: '-1px', zIndex: 2, position: 'relative', overflow: 'visible' };
|
|
36
|
+
const CELL_IN_RANGE_SX = { bgcolor: 'var(--ogrid-bg-range, rgba(33, 115, 70, 0.12))' };
|
|
37
|
+
const CELL_CUT_RANGE_SX = { bgcolor: 'action.hover', opacity: 0.7 };
|
|
38
|
+
// Pre-computed overlay variant arrays (avoid per-cell array allocation + filter)
|
|
39
|
+
// Key: `${base}_${overlay}` where overlay is 'active' | 'range' | 'cut'
|
|
40
|
+
const OVERLAY_VARIANTS = {
|
|
41
|
+
base_active: [CELL_CONTENT_BASE_SX, CELL_ACTIVE_SX],
|
|
42
|
+
base_range: [CELL_CONTENT_BASE_SX, CELL_IN_RANGE_SX],
|
|
43
|
+
base_cut: [CELL_CONTENT_BASE_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
44
|
+
editable_active: [CELL_CONTENT_EDITABLE_SX, CELL_ACTIVE_SX],
|
|
45
|
+
editable_range: [CELL_CONTENT_EDITABLE_SX, CELL_IN_RANGE_SX],
|
|
46
|
+
editable_cut: [CELL_CONTENT_EDITABLE_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
47
|
+
numeric_active: [CELL_CONTENT_NUMERIC_SX, CELL_ACTIVE_SX],
|
|
48
|
+
numeric_range: [CELL_CONTENT_NUMERIC_SX, CELL_IN_RANGE_SX],
|
|
49
|
+
numeric_cut: [CELL_CONTENT_NUMERIC_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
50
|
+
numeric_editable_active: [CELL_CONTENT_NUMERIC_EDITABLE_SX, CELL_ACTIVE_SX],
|
|
51
|
+
numeric_editable_range: [CELL_CONTENT_NUMERIC_EDITABLE_SX, CELL_IN_RANGE_SX],
|
|
52
|
+
numeric_editable_cut: [CELL_CONTENT_NUMERIC_EDITABLE_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
53
|
+
boolean_active: [CELL_CONTENT_BOOLEAN_SX, CELL_ACTIVE_SX],
|
|
54
|
+
boolean_range: [CELL_CONTENT_BOOLEAN_SX, CELL_IN_RANGE_SX],
|
|
55
|
+
boolean_cut: [CELL_CONTENT_BOOLEAN_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
56
|
+
boolean_editable_active: [CELL_CONTENT_BOOLEAN_EDITABLE_SX, CELL_ACTIVE_SX],
|
|
57
|
+
boolean_editable_range: [CELL_CONTENT_BOOLEAN_EDITABLE_SX, CELL_IN_RANGE_SX],
|
|
58
|
+
boolean_editable_cut: [CELL_CONTENT_BOOLEAN_EDITABLE_SX, CELL_IN_RANGE_SX, CELL_CUT_RANGE_SX],
|
|
59
|
+
};
|
|
60
|
+
/** Select pre-computed sx for a cell based on column type, editability, and overlay state. */
|
|
61
|
+
function getCellSx(colType, canEdit, isActive, isInRange, isInCutRange) {
|
|
62
|
+
// Determine base key
|
|
63
|
+
let baseKey;
|
|
64
|
+
if (colType === 'numeric')
|
|
65
|
+
baseKey = canEdit ? 'numeric_editable' : 'numeric';
|
|
66
|
+
else if (colType === 'boolean')
|
|
67
|
+
baseKey = canEdit ? 'boolean_editable' : 'boolean';
|
|
68
|
+
else
|
|
69
|
+
baseKey = canEdit ? 'editable' : 'base';
|
|
70
|
+
// Determine overlay
|
|
71
|
+
if (isInCutRange)
|
|
72
|
+
return OVERLAY_VARIANTS[`${baseKey}_cut`];
|
|
73
|
+
if (isInRange)
|
|
74
|
+
return OVERLAY_VARIANTS[`${baseKey}_range`];
|
|
75
|
+
if (isActive)
|
|
76
|
+
return OVERLAY_VARIANTS[`${baseKey}_active`];
|
|
77
|
+
// No overlay — return the base sx directly
|
|
78
|
+
if (colType === 'numeric')
|
|
79
|
+
return canEdit ? CELL_CONTENT_NUMERIC_EDITABLE_SX : CELL_CONTENT_NUMERIC_SX;
|
|
80
|
+
if (colType === 'boolean')
|
|
81
|
+
return canEdit ? CELL_CONTENT_BOOLEAN_EDITABLE_SX : CELL_CONTENT_BOOLEAN_SX;
|
|
82
|
+
return canEdit ? CELL_CONTENT_EDITABLE_SX : CELL_CONTENT_BASE_SX;
|
|
83
|
+
}
|
|
84
|
+
// Fill handle
|
|
85
|
+
const FILL_HANDLE_SX = {
|
|
86
|
+
position: 'absolute', right: -3, bottom: -3, width: 7, height: 7,
|
|
87
|
+
bgcolor: 'var(--ogrid-selection, #217346)', border: '1px solid var(--ogrid-bg, #fff)', borderRadius: '1px',
|
|
88
|
+
cursor: 'crosshair', pointerEvents: 'auto', zIndex: 3,
|
|
89
|
+
};
|
|
90
|
+
// Cell <td> positioning variants
|
|
91
|
+
const CELL_TD_BASE_SX = { position: 'relative', p: 0, height: '1px' };
|
|
92
|
+
const CELL_TD_PINNED_LEFT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', left: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform' };
|
|
93
|
+
const CELL_TD_PINNED_RIGHT_SX = { ...CELL_TD_BASE_SX, position: 'sticky', right: 0, zIndex: 6, bgcolor: 'background.paper', willChange: 'transform' };
|
|
94
|
+
// Header cell positioning variants
|
|
95
|
+
const HEADER_BASE_SX = { fontWeight: 600, position: 'relative' };
|
|
96
|
+
const HEADER_PINNED_LEFT_SX = { ...HEADER_BASE_SX, position: 'sticky', left: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform' };
|
|
97
|
+
const HEADER_PINNED_RIGHT_SX = { ...HEADER_BASE_SX, position: 'sticky', right: 0, top: 0, zIndex: 9, bgcolor: 'action.hover', willChange: 'transform' };
|
|
98
|
+
// Resize handle
|
|
99
|
+
const RESIZE_HANDLE_SX = {
|
|
100
|
+
position: 'absolute', top: 0, right: '-3px', bottom: 0, width: '8px',
|
|
101
|
+
cursor: 'col-resize', userSelect: 'none',
|
|
102
|
+
'&::after': { content: '""', position: 'absolute', top: 0, right: '3px', bottom: 0, width: '2px' },
|
|
103
|
+
'&:hover::after': { bgcolor: 'primary.main' },
|
|
104
|
+
'&:active::after': { bgcolor: 'primary.dark' },
|
|
105
|
+
};
|
|
106
|
+
// Popover
|
|
107
|
+
const POPOVER_ANCHOR_SX = { minHeight: '100%', minWidth: 40 };
|
|
108
|
+
const POPOVER_CONTENT_SX = { p: 1 };
|
|
109
|
+
// Wrapper
|
|
110
|
+
const WRAPPER_SCROLL_SX = { display: 'flex', flexDirection: 'column', minHeight: '100%' };
|
|
111
|
+
// Table wrapper
|
|
112
|
+
const TABLE_WRAPPER_SX = { position: 'relative', opacity: 1 };
|
|
113
|
+
const TABLE_WRAPPER_LOADING_SX = { position: 'relative', opacity: 0.6 };
|
|
114
|
+
// Empty state
|
|
115
|
+
const EMPTY_STATE_SX = { py: 4, px: 2, textAlign: 'center', borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' };
|
|
116
|
+
// Loading overlay
|
|
117
|
+
const LOADING_OVERLAY_SX = {
|
|
118
|
+
position: 'absolute', inset: 0, zIndex: 2,
|
|
119
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
120
|
+
bgcolor: 'rgba(255,255,255,0.7)',
|
|
121
|
+
};
|
|
122
|
+
const LOADING_INNER_SX = {
|
|
123
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1,
|
|
124
|
+
p: 2, bgcolor: 'background.paper', border: 1, borderColor: 'divider', borderRadius: 1,
|
|
125
|
+
};
|
|
126
|
+
// Module-scope event handlers
|
|
127
|
+
const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
128
|
+
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
129
|
+
const NOOP = () => { };
|
|
130
|
+
function GridRowInner(props) {
|
|
131
|
+
const { item, rowIndex, rowId, isSelected, columnLayouts, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
|
|
132
|
+
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}` }) }) })), columnLayouts.map((cl, colIdx) => (_jsx(TableCell, { sx: cl.tdSx, style: { minWidth: cl.minWidth, width: cl.width, maxWidth: cl.maxWidth }, children: renderCellContent(item, cl.col, rowIndex, colIdx) }, cl.col.columnId)))] }));
|
|
133
|
+
}
|
|
134
|
+
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
135
|
+
function DataGridTableInner(props) {
|
|
136
|
+
const wrapperRef = useRef(null);
|
|
137
|
+
const tableContainerRef = useRef(null);
|
|
138
|
+
const state = useDataGridState({ props, wrapperRef });
|
|
139
|
+
const lastMouseShiftRef = useRef(false);
|
|
140
|
+
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
|
|
141
|
+
const { visibleCols, hasCheckboxCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
142
|
+
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
143
|
+
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
144
|
+
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
145
|
+
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
146
|
+
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
147
|
+
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
148
|
+
const { items, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
149
|
+
const fitToContent = layoutMode === 'content';
|
|
150
|
+
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
151
|
+
// Memoize header rows (recursive tree traversal)
|
|
152
|
+
const headerRows = useMemo(() => buildHeaderRows(props.columns, props.visibleColumns), [props.columns, props.visibleColumns]);
|
|
153
|
+
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
154
|
+
columnSizingOverrides,
|
|
155
|
+
setColumnSizingOverrides,
|
|
156
|
+
});
|
|
157
|
+
// Pre-compute per-column layout (tdSx, widths) so GridRow doesn't recalculate per-cell
|
|
158
|
+
const columnLayouts = useMemo(() => visibleCols.map((col, colIdx) => {
|
|
159
|
+
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
160
|
+
const isPinnedLeft = col.pinned === 'left';
|
|
161
|
+
const isPinnedRight = col.pinned === 'right';
|
|
162
|
+
const columnWidth = getColumnWidth(col);
|
|
163
|
+
const tdSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? CELL_TD_PINNED_LEFT_SX : isPinnedRight ? CELL_TD_PINNED_RIGHT_SX : CELL_TD_BASE_SX;
|
|
164
|
+
return { col, tdSx, minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth };
|
|
165
|
+
}), [visibleCols, freezeCols, getColumnWidth]);
|
|
166
|
+
const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
|
|
167
|
+
const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
|
|
168
|
+
// Refs for volatile state — lets renderCellContent be stable (same function ref across
|
|
169
|
+
// selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
|
|
170
|
+
const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
|
|
171
|
+
const pendingEditorValueRef = useLatestRef(pendingEditorValue);
|
|
172
|
+
const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
|
|
173
|
+
// Stable row-click handler
|
|
174
|
+
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
175
|
+
const handleSingleRowClick = useCallback((e) => {
|
|
176
|
+
if (rowSelection !== 'single')
|
|
177
|
+
return;
|
|
178
|
+
const rowId = e.currentTarget.dataset.rowId;
|
|
179
|
+
if (!rowId)
|
|
180
|
+
return;
|
|
181
|
+
const ids = selectedRowIdsRef.current;
|
|
182
|
+
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
183
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
|
|
184
|
+
}, [rowSelection, updateSelection]);
|
|
185
|
+
// Wrapper sx (depends on dynamic values — memoize to avoid recreation)
|
|
186
|
+
const wrapperSx = useMemo(() => ({
|
|
187
|
+
position: 'relative',
|
|
188
|
+
flex: 1,
|
|
189
|
+
minHeight: 0,
|
|
190
|
+
width: fitToContent ? 'fit-content' : '100%',
|
|
191
|
+
maxWidth: '100%',
|
|
192
|
+
overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
|
|
193
|
+
overflowY: 'auto',
|
|
194
|
+
bgcolor: 'background.paper',
|
|
195
|
+
willChange: 'scroll-position',
|
|
196
|
+
'& [data-drag-range]': { bgcolor: 'rgba(33, 115, 70, 0.12) !important' },
|
|
197
|
+
}), [fitToContent, suppressHorizontalScroll, allowOverflowX]);
|
|
198
|
+
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
199
|
+
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
|
|
200
|
+
const rowId = getRowId(item);
|
|
201
|
+
let cellContent;
|
|
202
|
+
if (descriptor.mode === 'editing-inline') {
|
|
203
|
+
cellContent = _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
|
|
204
|
+
}
|
|
205
|
+
else if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
206
|
+
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValueRef.current, editCallbacks);
|
|
207
|
+
const CustomEditor = col.cellEditor;
|
|
208
|
+
cellContent = (_jsxs(_Fragment, { children: [_jsx(Box, { ref: (el) => { if (el)
|
|
209
|
+
setPopoverAnchorEl(el); }, sx: POPOVER_ANCHOR_SX, "aria-hidden": true }), _jsx(Popover, { open: !!popoverAnchorElRef.current, anchorEl: popoverAnchorElRef.current, onClose: cancelPopoverEdit, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, children: _jsx(Box, { sx: POPOVER_CONTENT_SX, children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
|
|
213
|
+
const cellStyle = resolveCellStyle(col, item);
|
|
214
|
+
const styledContent = cellStyle ? _jsx(Box, { component: "span", sx: cellStyle, children: content }) : content;
|
|
215
|
+
// Select pre-computed sx variant (module-scope = no per-cell allocation)
|
|
216
|
+
const cellSx = getCellSx(col.type, descriptor.canEditAny, descriptor.isActive && !descriptor.isInRange, descriptor.isInRange, descriptor.isInCutRange);
|
|
217
|
+
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
218
|
+
cellContent = (_jsxs(Box, { component: "div", ...interactionProps, sx: cellSx, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx(Box, { component: "div", onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle", sx: FILL_HANDLE_SX }))] }));
|
|
219
|
+
}
|
|
220
|
+
return (_jsx(CellErrorBoundary, { onError: onCellError, children: cellContent }, `${rowId}-${col.columnId}`));
|
|
221
|
+
},
|
|
222
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
223
|
+
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
224
|
+
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', 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: { overflow: 'hidden', minWidth: minTableWidth }, "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 })), row.map((cell, cellIdx) => {
|
|
225
|
+
if (cell.isGroup) {
|
|
226
|
+
return (_jsx(TableCell, { colSpan: cell.colSpan, component: "th", scope: "colgroup", sx: GROUP_HEADER_CELL_SX, children: cell.label }, cellIdx));
|
|
227
|
+
}
|
|
228
|
+
// Leaf cell
|
|
229
|
+
const col = cell.columnDef;
|
|
230
|
+
const colIdx = visibleCols.indexOf(col);
|
|
231
|
+
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
232
|
+
const isPinnedLeft = col.pinned === 'left';
|
|
233
|
+
const isPinnedRight = col.pinned === 'right';
|
|
234
|
+
const columnWidth = getColumnWidth(col);
|
|
235
|
+
const headerSx = isPinnedLeft || (isFreezeCol && colIdx === 0) ? HEADER_PINNED_LEFT_SX : isPinnedRight ? HEADER_PINNED_RIGHT_SX : HEADER_BASE_SX;
|
|
236
|
+
return (_jsxs(TableCell, { component: "th", scope: "col", rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined, sx: headerSx, style: { minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth }, children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx(Box, { onMouseDown: (e) => handleResizeStart(e, col), sx: RESIZE_HANDLE_SX })] }, col.columnId));
|
|
237
|
+
})] }, rowIdx))) }), !showEmptyInGrid && (_jsx(TableBody, { children: items.map((item, rowIndex) => {
|
|
238
|
+
const rowIdStr = getRowId(item);
|
|
239
|
+
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, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
|
|
240
|
+
}) }))] }), _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 &&
|
|
241
|
+
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)] }), 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 })] }) }))] }));
|
|
242
|
+
}
|
|
243
|
+
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Menu, MenuItem, Divider } from '@mui/material';
|
|
4
|
+
import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from '@alaarab/ogrid-react';
|
|
5
|
+
export function GridContextMenu(props) {
|
|
6
|
+
const { x, y, hasSelection, canUndo, canRedo, onClose } = props;
|
|
7
|
+
const handlers = React.useMemo(() => getContextMenuHandlers(props), [props]);
|
|
8
|
+
const isDisabled = React.useCallback((item) => {
|
|
9
|
+
if (item.disabledWhenNoSelection && !hasSelection)
|
|
10
|
+
return true;
|
|
11
|
+
if (item.id === 'undo' && !canUndo)
|
|
12
|
+
return true;
|
|
13
|
+
if (item.id === 'redo' && !canRedo)
|
|
14
|
+
return true;
|
|
15
|
+
return false;
|
|
16
|
+
}, [hasSelection, canUndo, canRedo]);
|
|
17
|
+
return (_jsx(Menu, { open: true, onClose: onClose, anchorReference: "anchorPosition", anchorPosition: { top: y, left: x }, MenuListProps: { dense: true, 'aria-label': 'Grid context menu' }, children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx(Divider, {}), _jsxs(MenuItem, { onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { style: { flex: 1 }, children: item.label }), item.shortcut && (_jsx("span", { style: { marginLeft: 24, color: 'rgba(0,0,0,0.4)', fontSize: '0.8em' }, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox, Select, MenuItem } from '@mui/material';
|
|
3
|
+
import { BaseInlineCellEditor, editorWrapperStyle } from '@alaarab/ogrid-react';
|
|
4
|
+
export function InlineCellEditor(props) {
|
|
5
|
+
return (_jsx(BaseInlineCellEditor, { ...props, renderCheckbox: (checked, onCommit, onCancel) => (_jsx(Checkbox, { checked: checked, onChange: (_, c) => onCommit(c), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), size: "small" })), renderSelect: (value, values, onCommit, onCancel) => (_jsx("div", { style: editorWrapperStyle, children: _jsx(Select, { size: "small", value: value !== null && value !== undefined ? String(value) : '', onChange: (e) => onCommit(e.target.value), onKeyDown: (e) => e.key === 'Escape' && (e.preventDefault(), onCancel()), autoFocus: true, sx: { minWidth: 0, flex: 1 }, children: values.map((v) => (_jsx(MenuItem, { value: String(v), children: String(v) }, String(v)))) }) })) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Typography } from '@mui/material';
|
|
3
|
+
import { getStatusBarParts } from '@alaarab/ogrid-react';
|
|
4
|
+
const partSx = (isLast) => ({
|
|
5
|
+
display: 'inline-flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
gap: 0.5,
|
|
8
|
+
...(isLast ? {} : { mr: 2, '&::after': { content: '"|"', ml: 2, color: 'divider' } }),
|
|
9
|
+
});
|
|
10
|
+
export function StatusBar(props) {
|
|
11
|
+
const parts = getStatusBarParts(props);
|
|
12
|
+
return (_jsx(Box, { role: "status", "aria-live": "polite", sx: { mt: 'auto', px: 1.5, py: 0.75, borderTop: 1, borderColor: 'divider', bgcolor: 'action.hover' }, children: parts.map((p, i) => (_jsxs(Typography, { component: "span", variant: "body2", sx: partSx(i === parts.length - 1), children: [_jsx(Typography, { component: "span", color: "text.secondary", children: p.label }), _jsx(Typography, { component: "span", fontWeight: 600, children: p.value.toLocaleString() })] }, p.key))) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OGrid, MaterialDataTable } from './MaterialDataTable';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useMemo, useCallback } from 'react';
|
|
4
|
+
import { IconButton, Button, Select, MenuItem, Box, Typography, } from '@mui/material';
|
|
5
|
+
import { FirstPage as FirstPageIcon, LastPage as LastPageIcon, ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, } from '@mui/icons-material';
|
|
6
|
+
import { getPaginationViewModel } from '@alaarab/ogrid-react';
|
|
7
|
+
export const PaginationControls = React.memo((props) => {
|
|
8
|
+
const { currentPage, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions, entityLabelPlural, className, } = props;
|
|
9
|
+
const labelPlural = entityLabelPlural ?? 'items';
|
|
10
|
+
const vm = useMemo(() => getPaginationViewModel(currentPage, pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined), [currentPage, pageSize, totalCount, pageSizeOptions]);
|
|
11
|
+
const handlePageSizeChange = useCallback((event) => {
|
|
12
|
+
onPageSizeChange(Number(event.target.value));
|
|
13
|
+
}, [onPageSizeChange]);
|
|
14
|
+
if (!vm) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const { pageNumbers, showStartEllipsis, showEndEllipsis, totalPages, startItem, endItem } = vm;
|
|
18
|
+
return (_jsxs(Box, { className: className, role: "navigation", "aria-label": "Pagination", sx: {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
gap: 2,
|
|
24
|
+
px: 1.5,
|
|
25
|
+
width: '100%',
|
|
26
|
+
minWidth: 0,
|
|
27
|
+
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: handlePageSizeChange, size: "small", "aria-label": "Rows per page", sx: { minWidth: 70 }, children: vm.pageSizeOptions.map((n) => (_jsx(MenuItem, { value: n, children: n }, n))) })] })] }));
|
|
29
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { OGrid, MaterialDataTable } from './MaterialDataTable';
|
|
3
|
+
export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
4
|
+
export { ColumnChooser } from './ColumnChooser/ColumnChooser';
|
|
5
|
+
export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
|
6
|
+
export { PaginationControls } from './PaginationControls/PaginationControls';
|
|
7
|
+
// Re-export everything from core
|
|
8
|
+
export * from '@alaarab/ogrid-react';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IColumnDefinition } from '@alaarab/ogrid-react';
|
|
3
|
+
export type { IColumnDefinition };
|
|
4
|
+
export interface IColumnChooserProps {
|
|
5
|
+
columns: IColumnDefinition[];
|
|
6
|
+
visibleColumns: Set<string>;
|
|
7
|
+
onVisibilityChange: (columnKey: string, visible: boolean) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const ColumnChooser: React.FC<IColumnChooserProps>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { UserLike, ColumnFilterType, IDateFilterValue } from '@alaarab/ogrid-react';
|
|
3
|
+
export interface IColumnHeaderFilterProps {
|
|
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
|
+
}
|
|
22
|
+
export declare const ColumnHeaderFilter: React.FC<IColumnHeaderFilterProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface MultiSelectFilterPopoverProps {
|
|
3
|
+
searchText: string;
|
|
4
|
+
onSearchChange: (value: string) => void;
|
|
5
|
+
options: string[];
|
|
6
|
+
filteredOptions: string[];
|
|
7
|
+
selected: Set<string>;
|
|
8
|
+
onOptionToggle: (option: string, checked: boolean) => void;
|
|
9
|
+
onSelectAll: () => void;
|
|
10
|
+
onClearSelection: () => void;
|
|
11
|
+
onApply: () => void;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const MultiSelectFilterPopover: React.FC<MultiSelectFilterPopoverProps>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { UserLike } from '@alaarab/ogrid-react';
|
|
3
|
+
export interface PeopleFilterPopoverProps {
|
|
4
|
+
selectedUser: UserLike | undefined;
|
|
5
|
+
searchText: string;
|
|
6
|
+
onSearchChange: (value: string) => void;
|
|
7
|
+
suggestions: UserLike[];
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
onUserSelect: (user: UserLike) => void;
|
|
10
|
+
onClearUser: () => void;
|
|
11
|
+
inputRef?: React.RefObject<HTMLInputElement | null>;
|
|
12
|
+
}
|
|
13
|
+
export declare const PeopleFilterPopover: React.FC<PeopleFilterPopoverProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnHeaderFilter } from './ColumnHeaderFilter';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { GridContextMenuHandlerProps } from '@alaarab/ogrid-react';
|
|
3
|
+
export interface GridContextMenuProps extends GridContextMenuHandlerProps {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
hasSelection: boolean;
|
|
7
|
+
canUndo: boolean;
|
|
8
|
+
canRedo: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function GridContextMenu(props: GridContextMenuProps): React.ReactElement;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IColumnDef } from '@alaarab/ogrid-react';
|
|
3
|
+
export interface InlineCellEditorProps<T> {
|
|
4
|
+
value: unknown;
|
|
5
|
+
item: T;
|
|
6
|
+
column: IColumnDef<T>;
|
|
7
|
+
rowIndex: number;
|
|
8
|
+
editorType: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
|
|
9
|
+
onCommit: (value: unknown) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function InlineCellEditor<T>(props: InlineCellEditorProps<T>): React.ReactElement;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface StatusBarProps {
|
|
3
|
+
totalCount: number;
|
|
4
|
+
filteredCount?: number;
|
|
5
|
+
selectedCount?: number;
|
|
6
|
+
selectedCellCount?: number;
|
|
7
|
+
aggregation?: {
|
|
8
|
+
sum: number;
|
|
9
|
+
avg: number;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
count: number;
|
|
13
|
+
} | null;
|
|
14
|
+
suppressRowCount?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type IOGridProps, type IOGridApi } from '@alaarab/ogrid-react';
|
|
3
|
+
export type { IOGridProps } from '@alaarab/ogrid-react';
|
|
4
|
+
declare const OGridInner: React.ForwardRefExoticComponent<IOGridProps<unknown> & React.RefAttributes<IOGridApi<unknown>>>;
|
|
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, MaterialDataTable, type IOGridProps } from './MaterialDataTable';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface IPaginationControlsProps {
|
|
3
|
+
currentPage: number;
|
|
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
|
+
}
|
|
12
|
+
export declare const PaginationControls: React.FC<IPaginationControlsProps>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OGrid, MaterialDataTable, type IOGridProps } from './MaterialDataTable';
|
|
2
|
+
export { DataGridTable } from './DataGridTable/DataGridTable';
|
|
3
|
+
export { ColumnChooser, type IColumnChooserProps } from './ColumnChooser/ColumnChooser';
|
|
4
|
+
export { ColumnHeaderFilter, type IColumnHeaderFilterProps } from './ColumnHeaderFilter/ColumnHeaderFilter';
|
|
5
|
+
export { PaginationControls, type IPaginationControlsProps } from './PaginationControls/PaginationControls';
|
|
6
|
+
export * from '@alaarab/ogrid-react';
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alaarab/ogrid-react-material",
|
|
3
|
+
"version": "2.0.0-beta",
|
|
4
|
+
"description": "OGrid Material UI implementation – MUI Table–based data grid with sorting, filtering, pagination, column chooser, spreadsheet selection, and CSV export.",
|
|
5
|
+
"main": "dist/esm/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"storybook": "storybook dev -p 6007 --no-open",
|
|
19
|
+
"build-storybook": "storybook build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ogrid",
|
|
23
|
+
"material-ui",
|
|
24
|
+
"mui",
|
|
25
|
+
"datatable",
|
|
26
|
+
"react",
|
|
27
|
+
"typescript",
|
|
28
|
+
"grid"
|
|
29
|
+
],
|
|
30
|
+
"author": "Ala Arab",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@alaarab/ogrid-react": "2.0.0-beta"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@emotion/react": "^11.0.0",
|
|
45
|
+
"@emotion/styled": "^11.0.0",
|
|
46
|
+
"@mui/icons-material": "^7.0.0",
|
|
47
|
+
"@mui/material": "^7.0.0",
|
|
48
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
49
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@emotion/react": "^11.14.0",
|
|
53
|
+
"@emotion/styled": "^11.14.0",
|
|
54
|
+
"@mui/icons-material": "^7.0.0",
|
|
55
|
+
"@mui/material": "^7.0.0",
|
|
56
|
+
"@storybook/react-vite": "10.2.8",
|
|
57
|
+
"storybook": "10.2.8",
|
|
58
|
+
"eslint-plugin-storybook": "10.2.8",
|
|
59
|
+
"vite": "^7.0.0"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|