Skip to content

Commit

Permalink
case sensitive icon in filter (#2216)
Browse files Browse the repository at this point in the history
* case sensitive icon in filter
resolves #2215
resolves #426

* support case sensitive filters

* Fab's comment

* fix file name

* test filters and match case

* lint

* fix test

* Fab

Co-authored-by: Fabien Lelaquais <[email protected]>

---------

Co-authored-by: Fred Lefévère-Laoide <[email protected]>
Co-authored-by: Fabien Lelaquais <[email protected]>
  • Loading branch information
3 people authored Nov 6, 2024
1 parent 633e01c commit 8f34308
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 89 deletions.
File renamed without changes.
104 changes: 42 additions & 62 deletions frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ describe("Table Filter Component", () => {
expect(getAllByText("Column")).toHaveLength(2);
expect(getAllByText("Action")).toHaveLength(2);
expect(getAllByText("Empty String")).toHaveLength(2);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
expect(getByTestId("CheckIcon").parentElement).toBeDisabled();
expect(getByTestId("DeleteIcon").parentElement).toBeDisabled();
});
Expand All @@ -81,12 +81,12 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("StringCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("contains"));
const validate = getByTestId("CheckIcon").parentElement;
Expand All @@ -98,12 +98,12 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("NumberCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("less equals"));
const validate = getByTestId("CheckIcon").parentElement;
Expand All @@ -121,19 +121,19 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("BoolCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("equals"));
const validate = getByTestId("CheckIcon").parentElement;
expect(validate).toBeDisabled();
const dddElts = getAllByTestId("ArrowDropDownIcon");
expect(dddElts).toHaveLength(3);
await userEvent.click(dddElts[2].parentElement?.firstElementChild || dddElts[0]);
const dddElements = getAllByTestId("ArrowDropDownIcon");
expect(dddElements).toHaveLength(3);
await userEvent.click(dddElements[2].parentElement?.firstElementChild || dddElements[0]);
await findByRole("listbox");
expect(validate).toBeDisabled();
await userEvent.click(getByText("True"));
Expand All @@ -145,12 +145,12 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("DateCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("before equal"));
const validate = getByTestId("CheckIcon").parentElement;
Expand All @@ -166,19 +166,19 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("StringCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("contains"));
const validate = getByTestId("CheckIcon");
expect(validate.parentElement).not.toBeDisabled();
await userEvent.click(validate);
const ddElts = getAllByTestId("ArrowDropDownIcon");
expect(ddElts).toHaveLength(4);
const ddElements = getAllByTestId("ArrowDropDownIcon");
expect(ddElements).toHaveLength(4);
getByText("1");
expect(onValidate).toHaveBeenCalled();
});
Expand All @@ -189,26 +189,26 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const dropdownElts = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElts).toHaveLength(2);
await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
const dropdownElements = getAllByTestId("ArrowDropDownIcon");
expect(dropdownElements).toHaveLength(2);
await userEvent.click(dropdownElements[0].parentElement?.firstElementChild || dropdownElements[0]);
await findByRole("listbox");
await userEvent.click(getByText("StringCol"));
await userEvent.click(dropdownElts[1].parentElement?.firstElementChild || dropdownElts[1]);
await userEvent.click(dropdownElements[1].parentElement?.firstElementChild || dropdownElements[1]);
await findByRole("listbox");
await userEvent.click(getByText("contains"));
const validate = getByTestId("CheckIcon");
expect(validate.parentElement).not.toBeDisabled();
await userEvent.click(validate);
const ddElts = getAllByTestId("ArrowDropDownIcon");
expect(ddElts).toHaveLength(4);
const ddElements = getAllByTestId("ArrowDropDownIcon");
expect(ddElements).toHaveLength(4);
const deletes = getAllByTestId("DeleteIcon");
expect(deletes).toHaveLength(2);
expect(deletes[0].parentElement).not.toBeDisabled();
expect(deletes[1].parentElement).toBeDisabled();
await userEvent.click(deletes[0]);
const ddElts2 = getAllByTestId("ArrowDropDownIcon");
expect(ddElts2).toHaveLength(2);
const ddElements2 = getAllByTestId("ArrowDropDownIcon");
expect(ddElements2).toHaveLength(2);
});
it("reset filters", async () => {
const onValidate = jest.fn();
Expand Down Expand Up @@ -242,13 +242,13 @@ describe("Table Filter Component", () => {
);
const elt = getByTestId("FilterListIcon");
await userEvent.click(elt);
const ddElts2 = getAllByTestId("ArrowDropDownIcon");
expect(ddElts2).toHaveLength(2);
const ddElements2 = getAllByTestId("ArrowDropDownIcon");
expect(ddElements2).toHaveLength(2);
});
});
describe("Table Filter Component - Case Insensitive Test", () => {
it("renders the case sensitivity toggle switch", async () => {
const { getByTestId, getAllByTestId, findByRole, getByText, getAllByText } = render(
it("renders the case sensitivity button", async () => {
const { getByTestId, getAllByTestId, findByRole, getByText, getByRole } = render(
<TableFilter columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} filteredCount={0} />
);

Expand All @@ -262,29 +262,9 @@ describe("Table Filter Component - Case Insensitive Test", () => {
await findByRole("listbox");
await userEvent.click(getByText("StringCol"));

// Select 'contains' filter action
await userEvent.click(dropdownIcons[1].parentElement?.firstElementChild || dropdownIcons[1]);
await findByRole("listbox");
await userEvent.click(getByText("contains"));

// Check for the case-sensitive toggle and interact with it
const caseSensitiveToggle = screen.getByRole("checkbox", { name: /case sensitive toggle/i });
expect(caseSensitiveToggle).toBeInTheDocument(); // Ensure the toggle is rendered
await userEvent.click(caseSensitiveToggle); // Toggle case sensitivity off

// Input some test text and validate case insensitivity
const inputs = getAllByText("Empty String");
const inputField = inputs[0].nextElementSibling?.firstElementChild || inputs[0];
await userEvent.click(inputField);
await userEvent.type(inputField, "CASETEST");

// Ensure the validate button is enabled
const validateButton = getByTestId("CheckIcon").parentElement;
expect(validateButton).not.toBeDisabled();

// Test case-insensitivity by changing input case
await userEvent.clear(inputField);
await userEvent.type(inputField, "casetest");
expect(validateButton).not.toBeDisabled();
const caseButton = getByRole("button", { name: /case insensitive/i });
expect(caseButton).toBeInTheDocument(); // Ensure the button is rendered
await userEvent.click(caseButton); // change case sensitivity
});
});
14 changes: 5 additions & 9 deletions frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import Popover, { PopoverOrigin } from "@mui/material/Popover";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import Switch from "@mui/material/Switch";
import { DateField, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";

Expand Down Expand Up @@ -305,14 +304,11 @@ const FilterRow = (props: FilterRowProps) => {
slotProps={{
input: {
endAdornment: (
<Switch
onChange={toggleMatchCase}
checked={matchCase}
size="small"
checkedIcon={<MatchCase />}
icon={<MatchCase color="disabled" />}
inputProps={{ "aria-label": "Case Sensitive Toggle" }}
/>
<Tooltip title={matchCase ? "Case sensitive" : "Case insensitive"}>
<IconButton onClick={toggleMatchCase} size="small">
<MatchCase color={matchCase ? "primary" : "disabled"} />
</IconButton>
</Tooltip>
),
},
}}
Expand Down
6 changes: 2 additions & 4 deletions frontend/taipy-gui/src/components/icons/MatchCase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import React from "react";
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";

export const MatchCase = (props: SvgIconProps) => (
<SvgIcon {...props} viewBox="0 0 16 16">
<g stroke="currentColor">
<path d="M20 14c0-1.5-.5-2-2-2h-2v-1c0-1 0-1-2-1v9h4c1.5 0 2-.53 2-2zm-8-2c0-1.5-.53-2-2-2H6c-1.5 0-2 .5-2 2v7h2v-3h4v3h2zm-2-5h4V5h-4zm12 2v11c0 1.11-.89 2-2 2H4a2 2 0 0 1-2-2V9c0-1.11.89-2 2-2h4V5l2-2h4l2 2v2h4a2 2 0 0 1 2 2m-6 8h2v-3h-2zM6 12h4v2H6z" />
</g>
<SvgIcon {...props} viewBox="0 0 24 24">
<path fill="currentColor" d="M20.06 18a4 4 0 0 1-.2-.89c-.67.7-1.48 1.05-2.41 1.05c-.83 0-1.52-.24-2.05-.71c-.53-.45-.8-1.06-.8-1.79c0-.88.33-1.56 1-2.05s1.61-.73 2.83-.73h1.4v-.64q0-.735-.45-1.17c-.3-.29-.75-.43-1.33-.43c-.52 0-.95.12-1.3.36c-.35.25-.52.54-.52.89h-1.46c0-.43.15-.84.45-1.24c.28-.4.71-.71 1.22-.94c.51-.21 1.06-.35 1.69-.35c.98 0 1.74.24 2.29.73s.84 1.16.86 2.02V16c0 .8.1 1.42.3 1.88V18zm-2.4-1.12c.45 0 .88-.11 1.29-.32c.4-.21.7-.49.88-.83v-1.57H18.7c-1.77 0-2.66.47-2.66 1.41c0 .43.15.73.46.96c.3.23.68.35 1.16.35m-12.2-3.17h4.07L7.5 8.29zM6.64 6h1.72l4.71 12h-1.93l-.97-2.57H4.82L3.86 18H1.93z"/>
</SvgIcon>
);
27 changes: 20 additions & 7 deletions taipy/gui_core/_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,20 @@ def get_hash():
}


def _filter_value(base_val: t.Any, operator: t.Callable, val: t.Any, adapt: t.Optional[t.Callable] = None):
def _filter_value(
base_val: t.Any,
operator: t.Callable,
val: t.Any,
adapt: t.Optional[t.Callable] = None,
match_case: bool = False,
):
if base_val is None:
base_val = "" if isinstance(val, str) else 0
else:
if isinstance(base_val, (datetime, date)):
base_val = base_val.isoformat()
val = adapt(base_val, val) if adapt else val
if isinstance(base_val, str) and isinstance(val, str):
if not match_case and isinstance(base_val, str) and isinstance(val, str):
base_val = base_val.lower()
val = val.lower()
return operator(base_val, val)
Expand All @@ -305,7 +311,7 @@ def _adapt_type(base_val, val):
return val


def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any):
def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any, match_case: bool = False):
if operator is contains:
types = {type(v) for v in list_val}
if len(types) == 1:
Expand All @@ -315,11 +321,18 @@ def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any):
else:
val = _adapt_type(typed_val, val)
return contains(list(list_val), val)
return next(filter(lambda v: _filter_value(v, operator, val), list_val), None) is not None
return next(filter(lambda v: _filter_value(v, operator, val, match_case=match_case), list_val), None) is not None


def _invoke_action(
ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn: t.Optional[str]
ent: t.Any,
col: str,
col_type: str,
is_dn: bool,
action: str,
val: t.Any,
col_fn: t.Optional[str] = None,
match_case: bool = False,
) -> bool:
if ent is None:
return False
Expand All @@ -337,8 +350,8 @@ def _invoke_action(
if isinstance(cur_val, DataNode):
cur_val = cur_val.read()
if not isinstance(cur_val, str) and isinstance(cur_val, Iterable):
return _filter_iterable(cur_val, op, val)
return _filter_value(cur_val, op, val, _adapt_type)
return _filter_iterable(cur_val, op, val, match_case)
return _filter_value(cur_val, op, val, _adapt_type, match_case)
except Exception as e:
if _is_debugging():
_warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
Expand Down
Loading

0 comments on commit 8f34308

Please sign in to comment.