Skip to content

Commit

Permalink
feat: split DataTable in several modules + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aldbr committed Apr 9, 2024
1 parent c41a3ee commit bd4e332
Show file tree
Hide file tree
Showing 10 changed files with 821 additions and 445 deletions.
404 changes: 27 additions & 377 deletions src/components/ui/DataTable.tsx

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions src/components/ui/FilterForm.tsx
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>
);
}
210 changes: 210 additions & 0 deletions src/components/ui/FilterToolbar.tsx
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>
</>
);
}
Loading

0 comments on commit bd4e332

Please sign in to comment.