-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: split DataTable in several modules + tests
- Loading branch information
Showing
10 changed files
with
821 additions
and
445 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import React from "react"; | ||
import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | ||
import { | ||
Box, | ||
FormControl, | ||
IconButton, | ||
InputLabel, | ||
MenuItem, | ||
Select, | ||
Stack, | ||
TextField, | ||
Tooltip, | ||
Typography, | ||
} from "@mui/material"; | ||
import { Filter } from "@/types/Filter"; | ||
import { Column } from "@/types/Column"; | ||
|
||
/** | ||
* Filter form props | ||
* @property {Column[]} columns - the columns on which to filter | ||
* @property {function} handleFilterChange - the function to call when a filter is changed | ||
* @property {function} handleFilterMenuClose - the function to call when the filter menu is closed | ||
* @property {Filter[]} filters - the filters for the table | ||
* @property {number} selectedFilterId - the id of the selected filter | ||
*/ | ||
interface FilterFormProps { | ||
columns: Column[]; | ||
handleFilterChange: (index: number, tempFilter: Filter) => void; | ||
handleFilterMenuClose: () => void; | ||
filters: Filter[]; | ||
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>; | ||
selectedFilterId: number | undefined; | ||
} | ||
|
||
/** | ||
* Filter form component | ||
* @param {FilterFormProps} props - the props for the component | ||
* @returns a FilterForm component | ||
*/ | ||
export function FilterForm(props: FilterFormProps) { | ||
const { | ||
columns, | ||
filters, | ||
setFilters, | ||
handleFilterChange, | ||
handleFilterMenuClose, | ||
selectedFilterId, | ||
} = props; | ||
const [tempFilter, setTempFilter] = React.useState<Filter | null>(null); | ||
|
||
// Find the index using the filter ID | ||
const filterIndex = filters.findIndex((f) => f.id === selectedFilterId); | ||
|
||
// Set the temp filter | ||
React.useEffect(() => { | ||
if (filterIndex !== -1) { | ||
setTempFilter(filters[filterIndex]); | ||
} else { | ||
setTempFilter({ id: Date.now(), column: "", operator: "eq", value: "" }); | ||
} | ||
}, [filters, filterIndex]); | ||
|
||
if (!tempFilter) return null; | ||
|
||
const onChange = (field: string, value: string) => { | ||
setTempFilter((prevFilter: Filter | null) => { | ||
if (prevFilter === null) { | ||
return null; // or initialize a new Filter object as appropriate | ||
} | ||
// Ensuring all fields of Filter are always defined | ||
const updatedFilter: Filter = { | ||
...prevFilter, | ||
[field]: value, | ||
}; | ||
return updatedFilter; | ||
}); | ||
}; | ||
|
||
const applyChanges = () => { | ||
if (filterIndex === -1) { | ||
setFilters([...filters, tempFilter]); | ||
} else { | ||
handleFilterChange(filterIndex, tempFilter); | ||
} | ||
handleFilterMenuClose(); | ||
}; | ||
|
||
return ( | ||
<Box sx={{ p: 2 }}> | ||
<Stack spacing={2} alignItems="flex-start"> | ||
<Typography variant="h6" padding={1}> | ||
Edit Filter | ||
</Typography> | ||
<Stack direction="row" spacing={2}> | ||
<FormControl variant="outlined" fullWidth> | ||
<InputLabel id="column">Column</InputLabel> | ||
<Select | ||
value={tempFilter.column} | ||
onChange={(e) => onChange("column", e.target.value)} | ||
label="Column" | ||
labelId="column" | ||
data-testid="filter-form-select-column" | ||
sx={{ minWidth: 120 }} | ||
> | ||
{columns.map((column) => ( | ||
<MenuItem key={column.id} value={column.id}> | ||
{column.label} | ||
</MenuItem> | ||
))} | ||
</Select> | ||
</FormControl> | ||
|
||
<FormControl variant="outlined" fullWidth> | ||
<InputLabel id="operator">Operator</InputLabel> | ||
<Select | ||
value={tempFilter.operator} | ||
onChange={(e) => onChange("operator", e.target.value)} | ||
label="Operator" | ||
labelId="operator" | ||
data-testid="filter-form-select-operator" | ||
sx={{ minWidth: 120 }} | ||
> | ||
<MenuItem value="eq">equals to</MenuItem> | ||
<MenuItem value="neq">not equals to</MenuItem> | ||
<MenuItem value="gt">is greater than</MenuItem> | ||
<MenuItem value="lt">is lower than</MenuItem> | ||
<MenuItem value="like">like</MenuItem> | ||
</Select> | ||
</FormControl> | ||
|
||
<FormControl variant="outlined" fullWidth> | ||
<TextField | ||
id="value" | ||
variant="outlined" | ||
label="Value" | ||
value={tempFilter.value} | ||
onChange={(e) => onChange("value", e.target.value)} | ||
sx={{ flexGrow: 1 }} | ||
/> | ||
</FormControl> | ||
|
||
<Tooltip title="Finish editing filter"> | ||
<IconButton onClick={() => applyChanges()} color="success"> | ||
<CheckCircleIcon /> | ||
</IconButton> | ||
</Tooltip> | ||
</Stack> | ||
</Stack> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import React from "react"; | ||
import FilterListIcon from "@mui/icons-material/FilterList"; | ||
import DeleteIcon from "@mui/icons-material/Delete"; | ||
import Chip from "@mui/material/Chip"; | ||
import Button from "@mui/material/Button"; | ||
import SendIcon from "@mui/icons-material/Send"; | ||
import { Popover, Stack, Tooltip } from "@mui/material"; | ||
import { FilterForm } from "./FilterForm"; | ||
import { Filter } from "@/types/Filter"; | ||
import { Column } from "@/types/Column"; | ||
|
||
/** | ||
* Filter toolbar component | ||
* @param {FilterToolbarProps} props - the props for the component | ||
*/ | ||
interface FilterToolbarProps { | ||
columns: Column[]; | ||
filters: Filter[]; | ||
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>; | ||
handleApplyFilters: () => void; | ||
} | ||
|
||
/** | ||
* Filter toolbar component | ||
* @param {FilterToolbarProps} props - the props for the component | ||
* @returns a FilterToolbar component | ||
*/ | ||
export function FilterToolbar(props: FilterToolbarProps) { | ||
const { columns, filters, setFilters, handleApplyFilters } = props; | ||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null); | ||
const [selectedFilter, setSelectedFilter] = React.useState<Filter | null>( | ||
null, | ||
); | ||
const addFilterButtonRef = React.useRef<HTMLButtonElement>(null); | ||
|
||
// Filter actions | ||
const handleAddFilter = React.useCallback(() => { | ||
// Create a new filter: it will not be used | ||
// It is just a placeholder to open the filter form | ||
const newFilter = { | ||
id: Date.now(), | ||
column: "", | ||
operator: "eq", | ||
value: "", | ||
}; | ||
setSelectedFilter(newFilter); | ||
setAnchorEl(addFilterButtonRef.current); | ||
}, [setSelectedFilter, setAnchorEl]); | ||
|
||
const handleRemoveAllFilters = React.useCallback(() => { | ||
setFilters([]); | ||
}, [setFilters]); | ||
|
||
const handleFilterChange = (index: number, newFilter: Filter) => { | ||
const updatedFilters = filters.map((filter, i) => | ||
i === index ? newFilter : filter, | ||
); | ||
setFilters(updatedFilters); | ||
}; | ||
|
||
const open = Boolean(anchorEl); | ||
|
||
// Filter menu | ||
/** | ||
* Handle the filter menu open | ||
* @param {React.MouseEvent<HTMLElement>} event - the event that triggered the menu open | ||
*/ | ||
const handleFilterMenuOpen = (event: React.MouseEvent<HTMLElement>) => { | ||
setAnchorEl(event.currentTarget); | ||
}; | ||
|
||
/** | ||
* Handle the filter menu close | ||
*/ | ||
const handleFilterMenuClose = () => { | ||
setAnchorEl(null); | ||
}; | ||
|
||
const handleRemoveFilter = (index: number) => { | ||
setFilters(filters.filter((_, i) => i !== index)); | ||
}; | ||
|
||
// Keyboard shortcuts | ||
React.useEffect(() => { | ||
function debounce(func: (...args: any[]) => void, wait: number) { | ||
let timeout: ReturnType<typeof setTimeout> | undefined; | ||
|
||
return function executedFunction(...args: any[]) { | ||
const later = () => { | ||
clearTimeout(timeout); | ||
func(...args); | ||
}; | ||
|
||
clearTimeout(timeout); | ||
timeout = setTimeout(later, wait); | ||
}; | ||
} | ||
|
||
const handleKeyPress = (event: { | ||
altKey: any; | ||
shiftKey: any; | ||
key: string; | ||
preventDefault: () => void; | ||
stopPropagation: () => void; | ||
}) => { | ||
if (event.altKey && event.shiftKey) { | ||
switch (event.key.toLowerCase()) { | ||
case "a": | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
handleAddFilter(); | ||
break; | ||
case "p": | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
handleApplyFilters(); | ||
break; | ||
case "c": | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
handleRemoveAllFilters(); | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
}; | ||
|
||
// Debounce the keypress handler to avoid rapid successive invocations | ||
const debouncedHandleKeyPress = debounce(handleKeyPress, 300); | ||
|
||
// Add event listener | ||
window.addEventListener("keydown", debouncedHandleKeyPress); | ||
|
||
// Remove event listener on cleanup | ||
return () => { | ||
window.removeEventListener("keydown", debouncedHandleKeyPress); | ||
}; | ||
}, [handleAddFilter, handleApplyFilters, handleRemoveAllFilters]); | ||
|
||
return ( | ||
<> | ||
<Stack direction="row" spacing={1} sx={{ m: 1 }}> | ||
<Tooltip title="Alt+Shift+a" placement="top"> | ||
<Button | ||
variant="text" | ||
startIcon={<FilterListIcon />} | ||
onClick={handleAddFilter} | ||
ref={addFilterButtonRef} | ||
> | ||
<span>Add filter</span> | ||
</Button> | ||
</Tooltip> | ||
<Tooltip title="Alt+Shift+p" placement="top"> | ||
<Button | ||
variant="text" | ||
startIcon={<SendIcon />} | ||
onClick={() => handleApplyFilters()} | ||
> | ||
<span>Apply filters</span> | ||
</Button> | ||
</Tooltip> | ||
<Tooltip title="Alt+Shift+c" placement="top"> | ||
<Button | ||
variant="text" | ||
startIcon={<DeleteIcon />} | ||
onClick={handleRemoveAllFilters} | ||
> | ||
<span>Clear all filters</span> | ||
</Button> | ||
</Tooltip> | ||
</Stack> | ||
<Stack direction="row" spacing={1} sx={{ m: 1, flexWrap: "wrap" }}> | ||
{filters.map((filter: Filter, index: number) => ( | ||
<Chip | ||
key={index} | ||
label={`${filter.column} ${filter.operator} ${filter.value}`} | ||
onClick={(event) => { | ||
handleFilterMenuOpen(event); // Open the menu | ||
setSelectedFilter(filter); // Set the selected filter | ||
}} | ||
onDelete={() => { | ||
handleRemoveFilter(index); | ||
}} | ||
color="primary" | ||
sx={{ m: 0.5 }} | ||
/> | ||
))} | ||
<Popover | ||
open={open} | ||
onClose={handleFilterMenuClose} | ||
anchorEl={anchorEl} | ||
anchorOrigin={{ | ||
vertical: "bottom", | ||
horizontal: "left", | ||
}} | ||
> | ||
<FilterForm | ||
columns={columns} | ||
handleFilterChange={handleFilterChange} | ||
handleFilterMenuClose={handleFilterMenuClose} | ||
filters={filters} | ||
setFilters={setFilters} | ||
selectedFilterId={selectedFilter?.id} | ||
/> | ||
</Popover> | ||
</Stack> | ||
</> | ||
); | ||
} |
Oops, something went wrong.