Skip to content

Commit

Permalink
Merge pull request #207 from Loxeris/feat_better_filter_ux
Browse files Browse the repository at this point in the history
feat: better filter ux
  • Loading branch information
aldbr authored Aug 28, 2024
2 parents 08022bf + 48818ff commit 7297b4e
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 81 deletions.
49 changes: 20 additions & 29 deletions packages/diracx-web-components/components/shared/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ export function DataTable(props: DataTableProps) {
const { getAllParam, getParam, setParam } = useSearchParamsUtils();
const appId = getParam("appId");

const [appliedFilters, setAppliedFilters] = React.useState<Filter[]>(filters);

const updateFiltersAndUrl = React.useCallback(
(newFilters: Filter[]) => {
// Update the filters in the URL using the setParam function
Expand Down Expand Up @@ -364,42 +366,27 @@ export function DataTable(props: DataTableProps) {
}));
setSearchBody({ search: jsonFilters });
setPage(0);
setAppliedFilters(filters);

// Update the filters in the URL
updateFiltersAndUrl(filters);
// Update the filters in the sections
updateSectionFilters(filters);
};

const SectionItem = React.useMemo(
() =>
sections
.find((section) => section.items.some((item) => item.id === appId))
?.items.find((item) => item.id === appId),
[appId, sections],
);

React.useEffect(() => {
// Function to parse the filters from the URL search params
const parseFiltersFromUrl = () => {
const filterStrings = getAllParam("filter");
return filterStrings.map((filterString: string) => {
const [id, column, operator, value] = filterString.split("_");
return { id: Number(id), column, operator, value };
});
};

const item = sections
.find((section) => section.items.some((item) => item.id === appId))
?.items.find((item) => item.id === appId);

if (getParam("filter")) {
// Parse the filters when the component mounts or when the searchParams change
const initialFilters = parseFiltersFromUrl();
// Set the filters (they will be displayed in the UI)
setFilters(initialFilters);
// Apply the filters to get the filtered data
const jsonFilters = initialFilters.map((filter) => ({
parameter: filter.column,
operator: filter.operator,
value: filter.value,
}));
setSearchBody({ search: jsonFilters });
} else if (item?.data?.filters) {
setFilters(item.data.filters);
const jsonFilters = item.data.filters.map(
if (SectionItem?.data?.filters) {
setFilters(SectionItem.data.filters);
setAppliedFilters(SectionItem.data.filters);
const jsonFilters = SectionItem.data.filters.map(
(filter: {
id: number;
column: string;
Expand All @@ -416,7 +403,7 @@ export function DataTable(props: DataTableProps) {
setFilters([]);
setSearchBody({ search: [] });
}
}, [appId, getAllParam, getParam, sections, setFilters, setSearchBody]);
}, [SectionItem?.data?.filters, setFilters, setSearchBody]);

// Manage sorting
const handleRequestSort = (
Expand Down Expand Up @@ -498,6 +485,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", p: 1 }} data-testid="skeleton">
Expand All @@ -520,6 +508,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", marginTop: 2 }}>
Expand All @@ -539,6 +528,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>
<Box sx={{ width: "100%", marginTop: 2 }}>
Expand All @@ -556,6 +546,7 @@ export function DataTable(props: DataTableProps) {
columns={columns}
filters={filters}
setFilters={setFilters}
appliedFilters={appliedFilters}
handleApplyFilters={handleApplyFilters}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ export const Default: Story = {
{ id: "name", label: "Name" },
{ id: "age", label: "Age" },
],
filters: [{ id: 0, column: "id", operator: "eq", value: "1" }],
filters: [
{ id: 0, column: "id", operator: "eq", value: "1" },
{ id: 1, column: "id", operator: "neq", value: "2" },
],
setFilters: () => {},
handleApplyFilters: () => {},
appliedFilters: [{ id: 0, column: "id", operator: "eq", value: "1" }],
},
render: (props) => {
const [, updateArgs] = useArgs();
const [{ filters }, updateArgs] = useArgs();
props.setFilters = (filters) => updateArgs({ filters });
props.handleApplyFilters = () => updateArgs({ appliedFilters: filters });
return <FilterToolbar {...props} />;
},
};
90 changes: 63 additions & 27 deletions packages/diracx-web-components/components/shared/FilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React from "react";
import { FilterList, Delete, Send } from "@mui/icons-material";
import Chip from "@mui/material/Chip";
import Button from "@mui/material/Button";
import { Popover, Stack, Tooltip } from "@mui/material";
import { Alert, Popover, Stack, Tooltip } from "@mui/material";
import { Filter } from "@/types/Filter";
import { Column } from "@/types/Column";
import { FilterForm } from "./FilterForm";
import "@/hooks/theme";

/**
* Filter toolbar component
Expand All @@ -18,6 +19,8 @@ interface FilterToolbarProps {
filters: Filter[];
/** The function to set the filters */
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>;
/** The applied filters */
appliedFilters: Filter[];
/** The function to apply the filters */
handleApplyFilters: () => void;
}
Expand All @@ -28,7 +31,8 @@ interface FilterToolbarProps {
* @returns a FilterToolbar component
*/
export function FilterToolbar(props: FilterToolbarProps) {
const { columns, filters, setFilters, handleApplyFilters } = props;
const { columns, filters, setFilters, appliedFilters, handleApplyFilters } =
props;
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const [selectedFilter, setSelectedFilter] = React.useState<Filter | null>(
null,
Expand Down Expand Up @@ -82,6 +86,17 @@ export function FilterToolbar(props: FilterToolbarProps) {
setFilters(filters.filter((_, i) => i !== index));
};

const changesUnapplied = React.useCallback(() => {
return JSON.stringify(filters) !== JSON.stringify(appliedFilters);
}, [filters, appliedFilters]);

const isApplied = React.useCallback(
(filter: Filter) => {
return appliedFilters.some((f) => f.id == filter.id);
},
[appliedFilters],
);

// Keyboard shortcuts
React.useEffect(() => {
function debounce(func: (...args: any[]) => void, wait: number) {
Expand Down Expand Up @@ -142,37 +157,51 @@ export function FilterToolbar(props: FilterToolbarProps) {

return (
<>
<Stack direction="row" spacing={1} sx={{ m: 1 }}>
<Stack direction="row" spacing={1} sx={{ m: 1 }} alignItems={"center"}>
<Tooltip title="Alt+Shift+a" placement="top">
<Button
variant="text"
startIcon={<FilterList />}
onClick={handleAddFilter}
ref={addFilterButtonRef}
>
<span>Add filter</span>
</Button>
<span>
<Button
variant="text"
startIcon={<FilterList />}
onClick={handleAddFilter}
ref={addFilterButtonRef}
>
<span>Add filter</span>
</Button>
</span>
</Tooltip>
<Tooltip title="Alt+Shift+p" placement="top">
<Button
variant="text"
startIcon={<Send />}
onClick={() => handleApplyFilters()}
>
<span>Apply filters</span>
</Button>
<span>
<Button
variant="text"
startIcon={<Send />}
onClick={() => handleApplyFilters()}
disabled={!changesUnapplied()}
>
<span>Apply filters</span>
</Button>
</span>
</Tooltip>
<Tooltip title="Alt+Shift+c" placement="top">
<Button
variant="text"
startIcon={<Delete />}
onClick={handleRemoveAllFilters}
>
<span>Clear all filters</span>
</Button>
<span>
<Button
variant="text"
startIcon={<Delete />}
onClick={handleRemoveAllFilters}
disabled={filters.length === 0}
>
<span>Clear all filters</span>
</Button>
</span>
</Tooltip>
</Stack>
<Stack direction="row" spacing={1} sx={{ m: 1, flexWrap: "wrap" }}>
<Stack
direction="row"
spacing={1}
flexWrap="wrap"
useFlexGap
sx={{ m: 1 }}
>
{filters.map((filter: Filter, index: number) => (
<Chip
key={index}
Expand All @@ -184,10 +213,11 @@ export function FilterToolbar(props: FilterToolbarProps) {
onDelete={() => {
handleRemoveFilter(index);
}}
color="primary"
color={isApplied(filter) ? "chipColor" : "default"}
sx={{ m: 0.5 }}
/>
))}

<Popover
open={open}
onClose={handleFilterMenuClose}
Expand All @@ -207,6 +237,12 @@ export function FilterToolbar(props: FilterToolbarProps) {
/>
</Popover>
</Stack>
{changesUnapplied() && (
<Alert severity="info">
Some filter changes have not been applied. Please click on &quot;Apply
filters&quot; to update your results.
</Alert>
)}
</>
);
}
14 changes: 10 additions & 4 deletions packages/diracx-web-components/contexts/ApplicationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { createContext, useCallback, useEffect, useState } from "react";
import React, {
createContext,
useCallback,
useMemo,
useEffect,
useState,
} from "react";
import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material";
import JSONCrush from "jsoncrush";
import { useSearchParamsUtils } from "@/hooks/searchParamsUtils";
Expand Down Expand Up @@ -36,9 +42,6 @@ export const ApplicationsProvider = ({

const { getParam, setParam } = useSearchParamsUtils();

// get user sections from searchParams
const sectionsParams = getParam("sections");

// save user sections to searchParams (but not icons)
const setSectionsParams = useCallback(
(sections: UserSection[] | ((prev: UserSection[]) => UserSection[])) => {
Expand All @@ -61,6 +64,9 @@ export const ApplicationsProvider = ({
[setParam, userSections],
);

// get user sections from searchParams
const sectionsParams = useMemo(() => getParam("sections"), [getParam]);

useEffect(() => {
if (userSections.length !== 0) return;
if (sectionsParams) {
Expand Down
46 changes: 34 additions & 12 deletions packages/diracx-web-components/hooks/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import { PaletteMode } from "@mui/material";
import { grey, lightGreen, cyan } from "@mui/material/colors";
import { createTheme, darken, lighten } from "@mui/material/styles";
import { cyan, grey, lightGreen } from "@mui/material/colors";
import {
createTheme,
darken,
lighten,
getContrastRatio,
} from "@mui/material/styles";
import { useContext } from "react";
import { ThemeContext } from "@/contexts/ThemeProvider";

declare module "@mui/material/styles" {
interface Palette {
chipColor: Palette["primary"];
}

interface PaletteOptions {
chipColor?: PaletteOptions["primary"];
}
}

declare module "@mui/material/Chip" {
interface ChipPropsColorOverrides {
chipColor: true;
}
}

/**
* Custom hook to access the theme context
* @returns the theme context
Expand All @@ -26,19 +47,27 @@ export const useTheme = () => {
export const useMUITheme = () => {
const { theme } = useTheme();

const primary = lightGreen[700];
const secondary = cyan[500];

const chipColor =
theme === "light" ? lighten(primary, 0.5) : darken(primary, 0.5);

// Create a Material-UI theme based on the current mode
const muiTheme = createTheme({
palette: {
mode: theme as PaletteMode,
primary: {
main: "#ffffff",
},
chipColor: {
main: chipColor,
contrastText:
getContrastRatio(chipColor, "#fff") > 4.5 ? "#fff" : "#111",
},
},
});

const primary = lightGreen[700];
const secondary = cyan[500];

const scrollbarBackground = theme === "dark" ? "#333" : "#f1f1f1";
const scrollbarThumbBackground = theme === "dark" ? "#888" : "#ccc";
const scrollbarThumbHoverBackground = theme === "dark" ? "#555" : "#999";
Expand Down Expand Up @@ -101,13 +130,6 @@ export const useMUITheme = () => {
},
},
},
MuiChip: {
styleOverrides: {
root: {
backgroundColor: lighten(primary, 0.75),
},
},
},
MuiInputLabel: {
styleOverrides: {
root: {
Expand Down
Loading

0 comments on commit 7297b4e

Please sign in to comment.