diff --git a/packages/diracx-web-components/components/shared/DataTable.tsx b/packages/diracx-web-components/components/shared/DataTable.tsx index 868bda83..15d29d9a 100644 --- a/packages/diracx-web-components/components/shared/DataTable.tsx +++ b/packages/diracx-web-components/components/shared/DataTable.tsx @@ -314,6 +314,8 @@ export function DataTable(props: DataTableProps) { const { getAllParam, getParam, setParam } = useSearchParamsUtils(); const appId = getParam("appId"); + const [appliedFilters, setAppliedFilters] = React.useState(filters); + const updateFiltersAndUrl = React.useCallback( (newFilters: Filter[]) => { // Update the filters in the URL using the setParam function @@ -364,6 +366,7 @@ export function DataTable(props: DataTableProps) { })); setSearchBody({ search: jsonFilters }); setPage(0); + setAppliedFilters(filters); // Update the filters in the URL updateFiltersAndUrl(filters); @@ -371,35 +374,19 @@ export function DataTable(props: DataTableProps) { 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; @@ -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 = ( @@ -498,6 +485,7 @@ export function DataTable(props: DataTableProps) { columns={columns} filters={filters} setFilters={setFilters} + appliedFilters={appliedFilters} handleApplyFilters={handleApplyFilters} /> @@ -520,6 +508,7 @@ export function DataTable(props: DataTableProps) { columns={columns} filters={filters} setFilters={setFilters} + appliedFilters={appliedFilters} handleApplyFilters={handleApplyFilters} /> @@ -539,6 +528,7 @@ export function DataTable(props: DataTableProps) { columns={columns} filters={filters} setFilters={setFilters} + appliedFilters={appliedFilters} handleApplyFilters={handleApplyFilters} /> @@ -556,6 +546,7 @@ export function DataTable(props: DataTableProps) { columns={columns} filters={filters} setFilters={setFilters} + appliedFilters={appliedFilters} handleApplyFilters={handleApplyFilters} /> diff --git a/packages/diracx-web-components/components/shared/FilterToolbar.stories.tsx b/packages/diracx-web-components/components/shared/FilterToolbar.stories.tsx index 1ec378b4..9d90c997 100644 --- a/packages/diracx-web-components/components/shared/FilterToolbar.stories.tsx +++ b/packages/diracx-web-components/components/shared/FilterToolbar.stories.tsx @@ -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 ; }, }; diff --git a/packages/diracx-web-components/components/shared/FilterToolbar.tsx b/packages/diracx-web-components/components/shared/FilterToolbar.tsx index 4a5d0c67..e5117024 100644 --- a/packages/diracx-web-components/components/shared/FilterToolbar.tsx +++ b/packages/diracx-web-components/components/shared/FilterToolbar.tsx @@ -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 @@ -18,6 +19,8 @@ interface FilterToolbarProps { filters: Filter[]; /** The function to set the filters */ setFilters: React.Dispatch>; + /** The applied filters */ + appliedFilters: Filter[]; /** The function to apply the filters */ handleApplyFilters: () => void; } @@ -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(null); const [selectedFilter, setSelectedFilter] = React.useState( null, @@ -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) { @@ -142,37 +157,51 @@ export function FilterToolbar(props: FilterToolbarProps) { return ( <> - + - + + + - + + + - + + + - + {filters.map((filter: Filter, index: number) => ( { handleRemoveFilter(index); }} - color="primary" + color={isApplied(filter) ? "chipColor" : "default"} sx={{ m: 0.5 }} /> ))} + + {changesUnapplied() && ( + + Some filter changes have not been applied. Please click on "Apply + filters" to update your results. + + )} ); } diff --git a/packages/diracx-web-components/contexts/ApplicationsProvider.tsx b/packages/diracx-web-components/contexts/ApplicationsProvider.tsx index 65ce51e0..5dd46d51 100644 --- a/packages/diracx-web-components/contexts/ApplicationsProvider.tsx +++ b/packages/diracx-web-components/contexts/ApplicationsProvider.tsx @@ -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"; @@ -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[])) => { @@ -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) { diff --git a/packages/diracx-web-components/hooks/theme.tsx b/packages/diracx-web-components/hooks/theme.tsx index 9d747405..53ce9f19 100644 --- a/packages/diracx-web-components/hooks/theme.tsx +++ b/packages/diracx-web-components/hooks/theme.tsx @@ -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 @@ -26,6 +47,12 @@ 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: { @@ -33,12 +60,14 @@ export const useMUITheme = () => { 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"; @@ -101,13 +130,6 @@ export const useMUITheme = () => { }, }, }, - MuiChip: { - styleOverrides: { - root: { - backgroundColor: lighten(primary, 0.75), - }, - }, - }, MuiInputLabel: { styleOverrides: { root: { diff --git a/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx b/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx index 0c9fe89e..fc372c83 100644 --- a/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx @@ -1,6 +1,9 @@ import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material"; import { FilterToolbar } from "@/components/shared/FilterToolbar"; +import { ThemeProvider } from "@/contexts/ThemeProvider"; +import { useMUITheme } from "@/hooks/theme"; describe("FilterToolbar", () => { const columns = [ @@ -12,17 +15,25 @@ describe("FilterToolbar", () => { { id: 1, column: "column1", operator: "eq", value: "value1" }, { id: 2, column: "column2", operator: "neq", value: "value2" }, ]; + const appliedFilters = [ + { id: 1, column: "column1", operator: "eq", value: "value1" }, + ]; const setFilters = jest.fn(); const handleApplyFilters = jest.fn(); beforeEach(() => { render( - , + + + + + , ); }); @@ -36,6 +47,48 @@ describe("FilterToolbar", () => { expect(clearAllFiltersButton).toBeInTheDocument(); }); + it("renders the chip with chipColor when the filter is applied", () => { + const chipApplied = screen.getByText("column1 eq value1").closest("div"); + const chipUnapplied = screen.getByText("column2 neq value2").closest("div"); + + expect(chipApplied).toHaveClass("MuiChip-colorChipColor"); + expect(chipUnapplied).not.toHaveClass("MuiChip-colorChipColor"); + }); + + it("renders the warning when there are unapplied filters", () => { + const warningMessage = screen.getByText( + 'Some filter changes have not been applied. Please click on "Apply filters" to update your results.', + ); + + expect(warningMessage).toBeInTheDocument(); + + appliedFilters.push({ + id: 2, + column: "column2", + operator: "neq", + value: "value2", + }); + + cleanup(); + + render( + + + + + , + ); + + expect(warningMessage).not.toBeInTheDocument(); + appliedFilters.pop(); + }); + it("opens the filter form when 'Add filter' button is clicked", () => { const addFilterButton = screen.getByText("Add filter"); @@ -70,3 +123,8 @@ describe("FilterToolbar", () => { expect(setFilters).toHaveBeenCalledWith([filters[1]]); }); }); + +function MUIProviders({ children }: { children: React.ReactNode }) { + const theme = useMUITheme(); + return {children}; +}