diff --git a/doc/gui/examples/controls/chat-streaming.py b/doc/gui/examples/controls/chat_streaming.py
similarity index 100%
rename from doc/gui/examples/controls/chat-streaming.py
rename to doc/gui/examples/controls/chat_streaming.py
diff --git a/frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx b/frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx
index fb0e96e385..4544afcbc2 100644
--- a/frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx
@@ -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();
});
@@ -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;
@@ -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;
@@ -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"));
@@ -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;
@@ -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();
});
@@ -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();
@@ -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(
);
@@ -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
});
});
diff --git a/frontend/taipy-gui/src/components/Taipy/TableFilter.tsx b/frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
index 50d57c65b6..b874e91246 100644
--- a/frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
@@ -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";
@@ -305,14 +304,11 @@ const FilterRow = (props: FilterRowProps) => {
slotProps={{
input: {
endAdornment: (
- }
- icon={}
- inputProps={{ "aria-label": "Case Sensitive Toggle" }}
- />
+
+
+
+
+
),
},
}}
diff --git a/frontend/taipy-gui/src/components/icons/MatchCase.tsx b/frontend/taipy-gui/src/components/icons/MatchCase.tsx
index e59dd72b0e..23342ade2e 100644
--- a/frontend/taipy-gui/src/components/icons/MatchCase.tsx
+++ b/frontend/taipy-gui/src/components/icons/MatchCase.tsx
@@ -15,9 +15,7 @@ import React from "react";
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
export const MatchCase = (props: SvgIconProps) => (
-
-
-
-
+
+
);
diff --git a/taipy/gui_core/_adapters.py b/taipy/gui_core/_adapters.py
index 579a8b6fd2..cffa66c716 100644
--- a/taipy/gui_core/_adapters.py
+++ b/taipy/gui_core/_adapters.py
@@ -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)
@@ -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:
@@ -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
@@ -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)
diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py
index 0a7aa69a33..522a143c82 100644
--- a/taipy/gui_core/_context.py
+++ b/taipy/gui_core/_context.py
@@ -284,10 +284,18 @@ def scenario_adapter(self, scenario: Scenario):
return None
def filter_entities(
- self, cycle_scenario: t.List, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn=None
+ self,
+ cycle_scenario: t.List,
+ col: str,
+ col_type: str,
+ is_dn: bool,
+ action: str,
+ val: t.Any,
+ col_fn=None,
+ match_case: bool = False,
):
cycle_scenario[2] = [
- e for e in cycle_scenario[2] if _invoke_action(e, col, col_type, is_dn, action, val, col_fn)
+ e for e in cycle_scenario[2] if _invoke_action(e, col, col_type, is_dn, action, val, col_fn, match_case)
]
return cycle_scenario
@@ -326,6 +334,7 @@ def get_filtered_scenario_list(
col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
val = fd.get("value")
action = fd.get("action", "")
+ match_case = fd.get("matchCase", False) is not False
customs = CustomScenarioFilter._get_custom(col)
if customs:
with self.gui._set_locals_context(customs[0] or None):
@@ -344,14 +353,14 @@ def get_filtered_scenario_list(
e
for e in filtered_list
if not isinstance(e, Scenario)
- or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn)
+ or _invoke_action(e, t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn, match_case)
]
# level 2 filtering
filtered_list = [
e
if isinstance(e, Scenario)
else self.filter_entities(
- t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn
+ t.cast(list, e), t.cast(str, col), col_type, is_datanode_prop, action, val, col_fn, match_case
)
for e in filtered_list
]
@@ -649,6 +658,7 @@ def get_filtered_datanode_list(
col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
val = fd.get("value")
action = fd.get("action", "")
+ match_case = fd.get("matchCase", False) is not False
customs = CustomScenarioFilter._get_custom(col)
if customs:
with self.gui._set_locals_context(customs[0] or None):
@@ -666,15 +676,17 @@ def get_filtered_datanode_list(
e
for e in filtered_list
if not isinstance(e, DataNode)
- or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn)
+ or _invoke_action(e, t.cast(str, col), col_type, False, action, val, col_fn, match_case)
]
# level 3 filtering
filtered_list = [
e
if isinstance(e, DataNode)
- else self.filter_entities(d, t.cast(str, col), col_type, False, action, val, col_fn)
+ else self.filter_entities(
+ t.cast(list, d), t.cast(str, col), col_type, False, action, val, col_fn, match_case
+ )
for e in filtered_list
- for d in t.cast(list, t.cast(list, e)[2])
+ for d in (t.cast(list, t.cast(list, e)[2]) if isinstance(e, list) else [e])
]
# remove empty cycles
return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))]
diff --git a/tests/gui_core/test_context_filter.py b/tests/gui_core/test_context_filter.py
new file mode 100644
index 0000000000..ac3bf528a8
--- /dev/null
+++ b/tests/gui_core/test_context_filter.py
@@ -0,0 +1,106 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import typing as t
+from unittest.mock import Mock, patch
+
+from taipy.common.config.common.scope import Scope
+from taipy.core import DataNode, Scenario
+from taipy.core.data.pickle import PickleDataNode
+from taipy.gui_core._context import _GuiCoreContext
+
+scenario_a = Scenario("scenario_a_config_id", None, {"a_prop": "a"})
+scenario_b = Scenario("scenario_b_config_id", None, {"a_prop": "b"})
+scenarios: t.List[t.Union[t.List, Scenario, None]] = [scenario_a, scenario_b]
+
+
+class TestGuiCoreContext_filter_scenarios:
+ def test_get_filtered_scenario_list_no_filter(self):
+ gui_core_context = _GuiCoreContext(Mock())
+ assert gui_core_context.get_filtered_scenario_list(scenarios, None) is scenarios
+
+ def test_get_filtered_scenario_list_a_filter(self):
+ gui_core_context = _GuiCoreContext(Mock())
+ res = gui_core_context.get_filtered_scenario_list(
+ scenarios, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains"}]
+ )
+ assert len(res) == 1
+ assert res[0] is scenario_a
+
+ def test_get_filtered_scenario_list_a_filter_case(self):
+ gui_core_context = _GuiCoreContext(Mock())
+ res = gui_core_context.get_filtered_scenario_list(
+ scenarios, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains", "matchCase": True}]
+ )
+ assert len(res) == 1
+ assert res[0] is scenario_a
+
+ res = gui_core_context.get_filtered_scenario_list(
+ scenarios, [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": False}]
+ )
+ assert len(res) == 1
+ assert res[0] is scenario_a
+
+ res = gui_core_context.get_filtered_scenario_list(
+ scenarios, [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": True}]
+ )
+ assert len(res) == 0
+
+
+datanode_a = PickleDataNode("datanode_a_config_id", Scope.SCENARIO)
+datanode_b = PickleDataNode("datanode_b_config_id", Scope.SCENARIO)
+datanodes: t.List[t.Union[t.List, DataNode, None]] = [datanode_a, datanode_b]
+
+
+def mock_core_get(entity_id):
+ if entity_id == datanode_a.id:
+ return datanode_a
+ if entity_id == datanode_b.id:
+ return datanode_b
+ return None
+
+
+class TestGuiCoreContext_filter_datanodes:
+ def test_get_filtered_datanode_list_no_filter(self):
+ gui_core_context = _GuiCoreContext(Mock())
+ assert gui_core_context.get_filtered_datanode_list(datanodes, None) is datanodes
+
+ def test_get_filtered_datanode_list_a_filter(self):
+ with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
+ gui_core_context = _GuiCoreContext(Mock())
+ res = gui_core_context.get_filtered_datanode_list(
+ datanodes, [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains"}]
+ )
+ assert len(res) == 1
+ assert res[0] is datanode_a
+
+ def test_get_filtered_datanode_list_a_filter_case(self):
+ with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
+ gui_core_context = _GuiCoreContext(Mock())
+ res = gui_core_context.get_filtered_datanode_list(
+ datanodes,
+ [{"col": "config_id", "type": "str", "value": "_a_", "action": "contains", "matchCase": True}],
+ )
+ assert len(res) == 1
+ assert res[0] is datanode_a
+
+ res = gui_core_context.get_filtered_datanode_list(
+ datanodes,
+ [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": False}],
+ )
+ assert len(res) == 1
+ assert res[0] is datanode_a
+
+ res = gui_core_context.get_filtered_datanode_list(
+ datanodes,
+ [{"col": "config_id", "type": "str", "value": "_A_", "action": "contains", "matchCase": True}],
+ )
+ assert len(res) == 0