Parcourir la source

Merge branch 'develop' into refactor/expand-Event-and-get_manager-method

Jean-Robin il y a 6 mois
Parent
commit
2b59af8ae4

+ 46 - 0
README.md

@@ -7,6 +7,52 @@
   </a>
   </a>
 </div>
 </div>
 
 
+<br>
+<div align="center">
+    <img 
+        src="https://img.shields.io/github/license/Avaiga/taipy?style=plastic&color=ff371a&labelColor=1f1f1f" 
+        alt="GitHub License" 
+        height="20px" 
+    />
+    <a target="_blank" href="https://github.com/Avaiga/taipy/releases">
+        <img 
+            alt="GitHub Release" 
+            height="20px" 
+            src="https://img.shields.io/github/v/release/Avaiga/taipy?display_name=release&style=plastic&color=ff371a&labelColor=1f1f1f"
+        ></a>
+</div>
+<br>
+<div align="center">
+   <img 
+      src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-ff371a?style=plastic&labelColor=1f1f1f" 
+        alt="Python version needed: 3.9" 
+    />
+  
+</div>
+<br>
+
+<div align="center">
+    <a target="_blank" href="https://docs.taipy.io/en/latest/">
+        <img 
+            src="https://img.shields.io/badge/docs-ff371a?style=plastic&labelColor=1f1f1f&label=Explore" 
+            height="20px" 
+            alt="Explore the docs" 
+        ></a>
+       <a target="_blank" href="https://docs.taipy.io/en/latest/gallery/">
+        <img 
+            src="https://img.shields.io/badge/gallery-ff371a?style=plastic&labelColor=1f1f1f&label=Explore" 
+            height="20px" 
+            alt="Explore Gallery" 
+        ></a>
+    <a target="_blank" href="https://discord.com/invite/SJyz2VJGxV">
+        <img 
+            src="https://img.shields.io/discord/1125797687476887563?style=plastic&labelColor=1f1f1f&logo=discord&logoColor=ff371a&label=Discord&color=ff371a" 
+            height="20px" 
+            alt="Discord support" 
+        ></a>
+</div>
+<br>
+
 <h1 align="center">
 <h1 align="center">
     Build Python Data & AI web applications
     Build Python Data & AI web applications
 </h1>
 </h1>

+ 0 - 0
doc/gui/examples/controls/chat-streaming.py → doc/gui/examples/controls/chat_streaming.py


+ 36 - 0
doc/gui/examples/controls/text_latex.py

@@ -0,0 +1,36 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+latex = """
+# Generated by *Taipy*
+
+You can insert *LaTeX* in a `text` control to
+display math equations.
+
+Displayed Equations:
+$$Attention(Q, K, V) = softmax(\\frac{QK^T}{\\sqrt{d_k}})V$$
+
+Inline Equations:
+Each head $h_i$ is the attention function of $\\textbf{Query}$, $\\textbf{Key}$ and $\\textbf{Value}$ with trainable parameters ($W_i^Q$, $W_i^K$, $W_i^V$)
+"""  # noqa W291
+
+page = """
+<|{latex}|text|mode=latex|>
+"""
+
+if __name__ == "__main__":
+    Gui(page).run(title="Text - Latex mode")

+ 2 - 2
frontend/taipy-gui/jest.config.js

@@ -27,7 +27,7 @@ module.exports = {
     ],
     ],
     coverageReporters: ["json", "html", "text"],
     coverageReporters: ["json", "html", "text"],
     modulePathIgnorePatterns: ["<rootDir>/packaging/"],
     modulePathIgnorePatterns: ["<rootDir>/packaging/"],
-    moduleNameMapper: {"react-markdown": "<rootDir>/node_modules/react-markdown/react-markdown.min.js"},
-    transformIgnorePatterns: ["<rootDir>/node_modules/(?!react-jsx-parser|react-markdown/)"],
+    moduleNameMapper: {"react-markdown": "<rootDir>/test-config/markdown.tsx"},
+    transformIgnorePatterns: ["<rootDir>/node_modules/(?!react-jsx-parser/)"],
     ...createJsWithTsPreset()
     ...createJsWithTsPreset()
 };
 };

Fichier diff supprimé car celui-ci est trop grand
+ 171 - 544
frontend/taipy-gui/package-lock.json


+ 1 - 0
frontend/taipy-gui/package.json

@@ -11,6 +11,7 @@
     "@mui/x-tree-view": "^7.0.0",
     "@mui/x-tree-view": "^7.0.0",
     "apache-arrow": "^17.0.0",
     "apache-arrow": "^17.0.0",
     "axios": "^1.2.0",
     "axios": "^1.2.0",
+    "better-react-mathjax": "^2.0.3",
     "date-fns": "^3.6.0",
     "date-fns": "^3.6.0",
     "date-fns-tz": "^3.1.3",
     "date-fns-tz": "^3.1.3",
     "lodash": "^4.17.21",
     "lodash": "^4.17.21",

+ 23 - 5
frontend/taipy-gui/src/components/Taipy/Field.spec.tsx

@@ -61,13 +61,31 @@ describe("Field Component", () => {
         expect(elt).toHaveStyle("width: 500px");
         expect(elt).toHaveStyle("width: 500px");
     });
     });
     it("can render markdown", async () => {
     it("can render markdown", async () => {
-        render(<Field value="titi" className="taipy-text" mode="md" />);
-        const elt = document.querySelector(".taipy-text");
-        await waitFor(() => expect(elt?.querySelector("p")).not.toBeNull());
+        const { container, getByText, findByText } = render(<Field value="titi" className="taipy-text" mode="md" />);
+        getByText(/markdown/i);
+        // https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
+        // expect(await findByText(/titi/i)).toBeInTheDocument();
     });
     });
     it("can render pre", async () => {
     it("can render pre", async () => {
-        render(<Field value="titi" className="taipy-text" mode="pre" />);
-        const elt = document.querySelector("pre.taipy-text");
+        const { container } = render(<Field value="titi" className="taipy-text" mode="pre" />);
+        const elt = container.querySelector("pre.taipy-text-pre");
         expect(elt).toBeInTheDocument();
         expect(elt).toBeInTheDocument();
     });
     });
+    describe("latex mode", () => {
+        it("renders LaTeX as block math", async () => {
+            const { container, getByText } = render(
+                <Field value={"$$x = y + 1$$"} className="taipy-text" mode="latex" />
+            );
+            getByText(/latex/i);
+            await waitFor(() => expect(container.querySelector(".taipy-text-latex")).toBeInTheDocument());
+        });
+        it("renders LaTeX as inline math", async () => {
+            const { container, getByText, findByText } = render(
+                <Field value={"This is inline $x = y + 1$ math."} className="taipy-text" mode="latex" />
+            );
+            // getByText(/latex/i); // already loaded ?
+            await waitFor(() => expect(container.querySelector(".taipy-text-latex")).toBeInTheDocument());
+            expect(await findByText(/inline/i)).toBeInTheDocument();
+        });
+    });
 });
 });

+ 60 - 5
frontend/taipy-gui/src/components/Taipy/Field.tsx

@@ -11,11 +11,12 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { lazy, useMemo } from "react";
+import React, { lazy, useMemo, Suspense } from "react";
 import Typography from "@mui/material/Typography";
 import Typography from "@mui/material/Typography";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
 
 
 import { formatWSValue } from "../../utils";
 import { formatWSValue } from "../../utils";
+import { getSuffixedClassNames } from "./utils";
 import { useClassNames, useDynamicProperty, useFormatConfig } from "../../utils/hooks";
 import { useClassNames, useDynamicProperty, useFormatConfig } from "../../utils/hooks";
 import { TaipyBaseProps, TaipyHoverProps, getCssSize } from "./utils";
 import { TaipyBaseProps, TaipyHoverProps, getCssSize } from "./utils";
 import { getComponentClassName } from "./TaipyStyle";
 import { getComponentClassName } from "./TaipyStyle";
@@ -33,6 +34,23 @@ interface TaipyFieldProps extends TaipyBaseProps, TaipyHoverProps {
 const unsetWeightSx = { fontWeight: "unset" };
 const unsetWeightSx = { fontWeight: "unset" };
 
 
 const Markdown = lazy(() => import("react-markdown"));
 const Markdown = lazy(() => import("react-markdown"));
+const MathJax = lazy(() => import("better-react-mathjax").then((module) => ({ default: module.MathJax })));
+const MathJaxContext = lazy(() =>
+    import("better-react-mathjax").then((module) => ({ default: module.MathJaxContext }))
+);
+
+const mathJaxConfig = {
+    tex: {
+        inlineMath: [
+            ["$", "$"],
+            ["\\(", "\\)"],
+        ],
+        displayMath: [
+            ["$$", "$$"],
+            ["\\[", "\\]"],
+        ],
+    },
+};
 
 
 const Field = (props: TaipyFieldProps) => {
 const Field = (props: TaipyFieldProps) => {
     const { id, dataType, format, defaultValue, raw } = props;
     const { id, dataType, format, defaultValue, raw } = props;
@@ -68,18 +86,55 @@ const Field = (props: TaipyFieldProps) => {
         <Tooltip title={hover || ""}>
         <Tooltip title={hover || ""}>
             <>
             <>
                 {mode == "pre" ? (
                 {mode == "pre" ? (
-                    <pre className={`${className} ${getComponentClassName(props.children)}`} id={id} style={style}>
+                    <pre
+                        className={`${className} ${getSuffixedClassNames(className, "-pre")} ${getComponentClassName(
+                            props.children
+                        )}`}
+                        id={id}
+                        style={style}
+                    >
                         {value}
                         {value}
                     </pre>
                     </pre>
                 ) : mode == "markdown" || mode == "md" ? (
                 ) : mode == "markdown" || mode == "md" ? (
-                    <Markdown className={`${className} ${getComponentClassName(props.children)}`}>{value}</Markdown>
+                    <Suspense fallback={<div>Loading Markdown...</div>}>
+                        <Markdown
+                            className={`${className} ${getSuffixedClassNames(
+                                className,
+                                "-markdown"
+                            )} ${getComponentClassName(props.children)}`}
+                        >
+                            {value}
+                        </Markdown>
+                    </Suspense>
                 ) : raw || mode == "raw" ? (
                 ) : raw || mode == "raw" ? (
-                    <span className={className} id={id} style={style}>
+                    <span
+                        className={`${className} ${getSuffixedClassNames(className, "-raw")} ${getComponentClassName(
+                            props.children
+                        )}`}
+                        id={id}
+                        style={style}
+                    >
                         {value}
                         {value}
                     </span>
                     </span>
+                ) : mode == "latex" ? (
+                    <Suspense fallback={<div>Loading LaTex...</div>}>
+                        <MathJaxContext config={mathJaxConfig}>
+                            <MathJax
+                                className={`${className} ${getSuffixedClassNames(
+                                    className,
+                                    "-latex"
+                                )} ${getComponentClassName(props.children)}`}
+                                id={id}
+                            >
+                                {value}
+                            </MathJax>
+                        </MathJaxContext>
+                    </Suspense>
                 ) : (
                 ) : (
                     <Typography
                     <Typography
-                        className={`${className} ${getComponentClassName(props.children)}`}
+                        className={`${className} ${
+                            mode ? getSuffixedClassNames(className, "-" + mode) : ""
+                        } ${getComponentClassName(props.children)}`}
                         id={id}
                         id={id}
                         component="span"
                         component="span"
                         sx={typoSx}
                         sx={typoSx}

+ 42 - 62
frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx

@@ -70,8 +70,8 @@ describe("Table Filter Component", () => {
         expect(getAllByText("Column")).toHaveLength(2);
         expect(getAllByText("Column")).toHaveLength(2);
         expect(getAllByText("Action")).toHaveLength(2);
         expect(getAllByText("Action")).toHaveLength(2);
         expect(getAllByText("Empty String")).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("CheckIcon").parentElement).toBeDisabled();
         expect(getByTestId("DeleteIcon").parentElement).toBeDisabled();
         expect(getByTestId("DeleteIcon").parentElement).toBeDisabled();
     });
     });
@@ -81,12 +81,12 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("StringCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("contains"));
         await userEvent.click(getByText("contains"));
         const validate = getByTestId("CheckIcon").parentElement;
         const validate = getByTestId("CheckIcon").parentElement;
@@ -98,12 +98,12 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("NumberCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("less equals"));
         await userEvent.click(getByText("less equals"));
         const validate = getByTestId("CheckIcon").parentElement;
         const validate = getByTestId("CheckIcon").parentElement;
@@ -121,19 +121,19 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("BoolCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("equals"));
         await userEvent.click(getByText("equals"));
         const validate = getByTestId("CheckIcon").parentElement;
         const validate = getByTestId("CheckIcon").parentElement;
         expect(validate).toBeDisabled();
         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");
         await findByRole("listbox");
         expect(validate).toBeDisabled();
         expect(validate).toBeDisabled();
         await userEvent.click(getByText("True"));
         await userEvent.click(getByText("True"));
@@ -145,12 +145,12 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("DateCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("before equal"));
         await userEvent.click(getByText("before equal"));
         const validate = getByTestId("CheckIcon").parentElement;
         const validate = getByTestId("CheckIcon").parentElement;
@@ -166,19 +166,19 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("StringCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("contains"));
         await userEvent.click(getByText("contains"));
         const validate = getByTestId("CheckIcon");
         const validate = getByTestId("CheckIcon");
         expect(validate.parentElement).not.toBeDisabled();
         expect(validate.parentElement).not.toBeDisabled();
         await userEvent.click(validate);
         await userEvent.click(validate);
-        const ddElts = getAllByTestId("ArrowDropDownIcon");
-        expect(ddElts).toHaveLength(4);
+        const ddElements = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElements).toHaveLength(4);
         getByText("1");
         getByText("1");
         expect(onValidate).toHaveBeenCalled();
         expect(onValidate).toHaveBeenCalled();
     });
     });
@@ -189,26 +189,26 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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 findByRole("listbox");
         await userEvent.click(getByText("StringCol"));
         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 findByRole("listbox");
         await userEvent.click(getByText("contains"));
         await userEvent.click(getByText("contains"));
         const validate = getByTestId("CheckIcon");
         const validate = getByTestId("CheckIcon");
         expect(validate.parentElement).not.toBeDisabled();
         expect(validate.parentElement).not.toBeDisabled();
         await userEvent.click(validate);
         await userEvent.click(validate);
-        const ddElts = getAllByTestId("ArrowDropDownIcon");
-        expect(ddElts).toHaveLength(4);
+        const ddElements = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElements).toHaveLength(4);
         const deletes = getAllByTestId("DeleteIcon");
         const deletes = getAllByTestId("DeleteIcon");
         expect(deletes).toHaveLength(2);
         expect(deletes).toHaveLength(2);
         expect(deletes[0].parentElement).not.toBeDisabled();
         expect(deletes[0].parentElement).not.toBeDisabled();
         expect(deletes[1].parentElement).toBeDisabled();
         expect(deletes[1].parentElement).toBeDisabled();
         await userEvent.click(deletes[0]);
         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 () => {
     it("reset filters", async () => {
         const onValidate = jest.fn();
         const onValidate = jest.fn();
@@ -242,13 +242,13 @@ describe("Table Filter Component", () => {
         );
         );
         const elt = getByTestId("FilterListIcon");
         const elt = getByTestId("FilterListIcon");
         await userEvent.click(elt);
         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", () => {
 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} />
             <TableFilter columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} filteredCount={0} />
         );
         );
 
 
@@ -262,29 +262,9 @@ describe("Table Filter Component - Case Insensitive Test", () => {
         await findByRole("listbox");
         await findByRole("listbox");
         await userEvent.click(getByText("StringCol"));
         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
         // 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
     });
     });
 });
 });

+ 5 - 9
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 Select, { SelectChangeEvent } from "@mui/material/Select";
 import TextField from "@mui/material/TextField";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
-import Switch from "@mui/material/Switch";
 import { DateField, LocalizationProvider } from "@mui/x-date-pickers";
 import { DateField, LocalizationProvider } from "@mui/x-date-pickers";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
 
 
@@ -305,14 +304,11 @@ const FilterRow = (props: FilterRowProps) => {
                         slotProps={{
                         slotProps={{
                             input: {
                             input: {
                                 endAdornment: (
                                 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>
                                 ),
                                 ),
                             },
                             },
                         }}
                         }}

+ 2 - 4
frontend/taipy-gui/src/components/icons/MatchCase.tsx

@@ -15,9 +15,7 @@ import React from "react";
 import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
 import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
 
 
 export const MatchCase = (props: SvgIconProps) => (
 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>
     </SvgIcon>
 );
 );

+ 11 - 0
frontend/taipy-gui/test-config/markdown.tsx

@@ -0,0 +1,11 @@
+import React, { ReactNode } from "react";
+
+interface ChildrenProps {
+    children: ReactNode;
+}
+
+function ReactMarkdownMock({ children }: ChildrenProps) {
+    return <p>{children}</p>;
+}
+
+export default ReactMarkdownMock;

+ 1 - 1
setup.py

@@ -56,7 +56,7 @@ setup(
             "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
             "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
         ],
         ],
         "rdp": ["rdp>=0.8"],
         "rdp": ["rdp>=0.8"],
-        "arrow": ["pyarrow>=17.0.0,<18.0"],
+        "arrow": ["pyarrow>=16.0.0,<18.0"],
         "mssql": ["pyodbc>=4"],
         "mssql": ["pyodbc>=4"],
     },
     },
     cmdclass={"build_py": NPMInstall},
     cmdclass={"build_py": NPMInstall},

+ 9 - 8
taipy/gui/state.py

@@ -11,7 +11,7 @@
 
 
 import inspect
 import inspect
 import typing as t
 import typing as t
-from abc import abstractmethod
+from abc import ABCMeta, abstractmethod
 from contextlib import nullcontext
 from contextlib import nullcontext
 from operator import attrgetter
 from operator import attrgetter
 from pathlib import Path
 from pathlib import Path
@@ -26,7 +26,7 @@ if t.TYPE_CHECKING:
     from .gui import Gui
     from .gui import Gui
 
 
 
 
-class State(SimpleNamespace):
+class State(SimpleNamespace, metaclass=ABCMeta):
     """Accessor to the bound variables from callbacks.
     """Accessor to the bound variables from callbacks.
 
 
     `State` is used when you need to access the value of variables
     `State` is used when you need to access the value of variables
@@ -114,9 +114,7 @@ class State(SimpleNamespace):
         val = attrgetter(name)(self)
         val = attrgetter(name)(self)
         _attrsetter(self, name, val)
         _attrsetter(self, name, val)
 
 
-    def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
-        return nullcontext()
-
+    @abstractmethod
     def broadcast(self, name: str, value: t.Any):
     def broadcast(self, name: str, value: t.Any):
         """Update a variable on all clients.
         """Update a variable on all clients.
 
 
@@ -127,9 +125,7 @@ class State(SimpleNamespace):
             name (str): The variable name to update.
             name (str): The variable name to update.
             value (Any): The new variable value.
             value (Any): The new variable value.
         """
         """
-        with self._set_context(self._gui):
-            encoded_name = self._gui._bind_var(name)
-            self._gui._broadcast_all_clients(encoded_name, value)
+        raise NotImplementedError
 
 
     def __enter__(self):
     def __enter__(self):
         self._gui.__enter__()
         self._gui.__enter__()
@@ -196,6 +192,11 @@ class _GuiState(State):
     def get_gui(self) -> "Gui":
     def get_gui(self) -> "Gui":
         return super().__getattribute__(_GuiState.__gui_attr)
         return super().__getattribute__(_GuiState.__gui_attr)
 
 
+    def broadcast(self, name: str, value: t.Any):
+        with self._set_context(self._gui):
+            encoded_name = self._gui._bind_var(name)
+            self._gui._broadcast_all_clients(encoded_name, value)
+
     def __getattribute__(self, name: str) -> t.Any:
     def __getattribute__(self, name: str) -> t.Any:
         if name == "__class__":
         if name == "__class__":
             return _GuiState
             return _GuiState

+ 16 - 0
taipy/gui/state_support.py

@@ -0,0 +1,16 @@
+# 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.
+
+from .state import State
+
+
+class StateSupport(State):
+    pass

+ 1 - 1
taipy/gui/viselements.json

@@ -23,7 +23,7 @@
                     {
                     {
                         "name": "mode",
                         "name": "mode",
                         "type": "str",
                         "type": "str",
-                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the <i>raw</i> property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
+                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the <i>raw</i> property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown</li><li>&quot;latex&quot;: LaTe&chi; support</li>"
                     },
                     },
                     {
                     {
                         "name": "format",
                         "name": "format",

+ 20 - 7
taipy/gui_core/_adapters.py

@@ -278,14 +278,20 @@ _operators: t.Dict[str, t.Callable] = {
 }
 }
 
 
 
 
-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:
     if base_val is None:
         base_val = "" if isinstance(val, str) else 0
         base_val = "" if isinstance(val, str) else 0
     else:
     else:
         if isinstance(base_val, (datetime, date)):
         if isinstance(base_val, (datetime, date)):
             base_val = base_val.isoformat()
             base_val = base_val.isoformat()
         val = adapt(base_val, val) if adapt else val
         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()
             base_val = base_val.lower()
             val = val.lower()
             val = val.lower()
     return operator(base_val, val)
     return operator(base_val, val)
@@ -305,7 +311,7 @@ def _adapt_type(base_val, val):
     return 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:
     if operator is contains:
         types = {type(v) for v in list_val}
         types = {type(v) for v in list_val}
         if len(types) == 1:
         if len(types) == 1:
@@ -315,11 +321,18 @@ def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any):
             else:
             else:
                 val = _adapt_type(typed_val, val)
                 val = _adapt_type(typed_val, val)
         return contains(list(list_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(
 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:
 ) -> bool:
     if ent is None:
     if ent is None:
         return False
         return False
@@ -337,8 +350,8 @@ def _invoke_action(
             if isinstance(cur_val, DataNode):
             if isinstance(cur_val, DataNode):
                 cur_val = cur_val.read()
                 cur_val = cur_val.read()
             if not isinstance(cur_val, str) and isinstance(cur_val, Iterable):
             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:
     except Exception as e:
         if _is_debugging():
         if _is_debugging():
             _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
             _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)

+ 19 - 7
taipy/gui_core/_context.py

@@ -284,10 +284,18 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
         return None
 
 
     def filter_entities(
     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] = [
         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
         return cycle_scenario
 
 
@@ -326,6 +334,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
             col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
             val = fd.get("value")
             val = fd.get("value")
             action = fd.get("action", "")
             action = fd.get("action", "")
+            match_case = fd.get("matchCase", False) is not False
             customs = CustomScenarioFilter._get_custom(col)
             customs = CustomScenarioFilter._get_custom(col)
             if customs:
             if customs:
                 with self.gui._set_locals_context(customs[0] or None):
                 with self.gui._set_locals_context(customs[0] or None):
@@ -344,14 +353,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 e
                 e
                 for e in filtered_list
                 for e in filtered_list
                 if not isinstance(e, Scenario)
                 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
             # level 2 filtering
             filtered_list = [
             filtered_list = [
                 e
                 e
                 if isinstance(e, Scenario)
                 if isinstance(e, Scenario)
                 else self.filter_entities(
                 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
                 for e in filtered_list
             ]
             ]
@@ -649,6 +658,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
             col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
             val = fd.get("value")
             val = fd.get("value")
             action = fd.get("action", "")
             action = fd.get("action", "")
+            match_case = fd.get("matchCase", False) is not False
             customs = CustomScenarioFilter._get_custom(col)
             customs = CustomScenarioFilter._get_custom(col)
             if customs:
             if customs:
                 with self.gui._set_locals_context(customs[0] or None):
                 with self.gui._set_locals_context(customs[0] or None):
@@ -666,15 +676,17 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 e
                 e
                 for e in filtered_list
                 for e in filtered_list
                 if not isinstance(e, DataNode)
                 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
             # level 3 filtering
             filtered_list = [
             filtered_list = [
                 e
                 e
                 if isinstance(e, DataNode)
                 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 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
         # remove empty cycles
         return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))]
         return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))]

+ 106 - 0
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

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff