Explorar o código

Merge branch 'develop' into bug/#1911-current-page-menu

Fred Lefévère-Laoide hai 7 meses
pai
achega
82b911c702
Modificáronse 36 ficheiros con 1072 adicións e 366 borrados
  1. 17 1
      .github/workflows/build-and-release.yml
  2. 1 1
      Pipfile
  3. 28 0
      doc/gui/examples/Alert.py
  4. 2 2
      frontend/taipy-gui/src/components/Router.tsx
  5. 26 176
      frontend/taipy-gui/src/components/Taipy/Alert.spec.tsx
  6. 32 66
      frontend/taipy-gui/src/components/Taipy/Alert.tsx
  7. 202 0
      frontend/taipy-gui/src/components/Taipy/Notification.spec.tsx
  8. 81 0
      frontend/taipy-gui/src/components/Taipy/Notification.tsx
  9. 46 3
      frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx
  10. 42 5
      frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
  11. 3 1
      frontend/taipy-gui/src/components/Taipy/index.ts
  12. 1 0
      frontend/taipy-gui/src/components/Taipy/tableUtils.tsx
  13. 23 0
      frontend/taipy-gui/src/components/icons/MatchCase.tsx
  14. 32 6
      frontend/taipy-gui/src/context/taipyReducers.spec.ts
  15. 15 5
      frontend/taipy-gui/src/context/taipyReducers.ts
  16. 53 11
      taipy/gui/_gui_cli.py
  17. 11 1
      taipy/gui/_page.py
  18. 16 0
      taipy/gui/_renderers/factory.py
  19. 13 4
      taipy/gui/_warnings.py
  20. 7 1
      taipy/gui/config.py
  21. 24 5
      taipy/gui/data/pandas_data_accessor.py
  22. 40 8
      taipy/gui/gui.py
  23. 11 1
      taipy/gui/gui_actions.py
  24. 13 3
      taipy/gui/utils/_evaluator.py
  25. 34 1
      taipy/gui/viselements.json
  26. 5 5
      taipy/gui_core/viselements.json
  27. 75 0
      tests/gui/data/test_pandas_data_accessor.py
  28. 24 0
      tests/gui/gui_specific/test_cli.py
  29. 1 1
      tests/gui/helpers.py
  30. 90 53
      tools/gui/generate_pyi.py
  31. 1 1
      tools/packages/pipfiles/Pipfile3.10.max
  32. 1 1
      tools/packages/pipfiles/Pipfile3.11.max
  33. 1 1
      tools/packages/pipfiles/Pipfile3.12.max
  34. 1 1
      tools/packages/pipfiles/Pipfile3.9.max
  35. 97 0
      tools/release/bump_version.py
  36. 3 2
      tools/release/setup_version.py

+ 17 - 1
.github/workflows/build-and-release.yml

@@ -20,6 +20,7 @@ env:
 
 permissions:
   contents: write
+  pull-requests: write
 
 jobs:
   fetch-versions:
@@ -238,11 +239,26 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
+      - name: Bump Version
+        if: github.event.inputs.release_type == 'dev'
+        id: bump-version
+        run: |
+          python tools/release/bump_version.py
+
       - uses: stefanzweifel/git-auto-commit-action@v5
+        if: github.event.inputs.release_type == 'dev'
         with:
-          file_pattern: '*/version.json'
+          branch: "feature/update-dev-version-${{ github.run_id }}"
+          create_branch: 'true'
+          file_pattern: '**/version.json'
           commit_message: Update version to ${{ needs.fetch-versions.outputs.NEW_VERSION }}
 
+      - name: create pull request
+        if: github.event.inputs.release_type == 'dev'
+        run: gh pr create -B develop -H "feature/update-dev-version-${{ github.run_id }}" --title 'Update Dev Version' --body 'Created by Github action'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
       - name: Reset changes
         run: |
           git reset --hard HEAD

+ 1 - 1
Pipfile

@@ -77,4 +77,4 @@ types-tzlocal = "*"
 python_version = "3"
 
 [pipenv]
-allow_prereleases = true
+allow_prereleases = false

+ 28 - 0
doc/gui/examples/Alert.py

@@ -0,0 +1,28 @@
+# 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
+
+severity = "error"
+variant = "filled"
+message = "This is an error message."
+
+page = """
+<|{message}|alert|severity={severity}|variant={variant}|>
+"""
+
+if __name__ == "__main__":
+    gui = Gui(page)
+    gui.run(title="Test Alert")

+ 2 - 2
frontend/taipy-gui/src/components/Router.tsx

@@ -34,10 +34,10 @@ import {
     taipyInitialize,
     taipyReducer,
 } from "../context/taipyReducers";
-import Alert from "./Taipy/Alert";
 import UIBlocker from "./Taipy/UIBlocker";
 import Navigate from "./Taipy/Navigate";
 import Menu from "./Taipy/Menu";
+import TaipyNotification from "./Taipy/Notification";
 import GuiDownload from "./Taipy/GuiDownload";
 import ErrorFallback from "../utils/ErrorBoundary";
 import MainPage from "./pages/MainPage";
@@ -152,7 +152,7 @@ const Router = () => {
                                         ) : null}
                                     </Box>
                                     <ErrorBoundary FallbackComponent={ErrorFallback}>
-                                        <Alert alerts={state.alerts} />
+                                        <TaipyNotification alerts={state.alerts} />
                                         <UIBlocker block={state.block} />
                                         <Navigate
                                             to={state.navigateTo}

+ 26 - 176
frontend/taipy-gui/src/components/Taipy/Alert.spec.tsx

@@ -12,191 +12,41 @@
  */
 
 import React from "react";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render } from "@testing-library/react";
 import "@testing-library/jest-dom";
-import { SnackbarProvider } from "notistack";
+import TaipyAlert from "./Alert";
 
-import Alert from "./Alert";
-import { AlertMessage } from "../../context/taipyReducers";
-import userEvent from "@testing-library/user-event";
-
-const defaultMessage = "message";
-const defaultAlerts: AlertMessage[] = [{ atype: "success", message: defaultMessage, system: true, duration: 3000 }];
-const getAlertsWithType = (aType: string) => [{ ...defaultAlerts[0], atype: aType }];
-
-class myNotification {
-    static requestPermission = jest.fn(() => Promise.resolve("granted"));
-    static permission = "granted";
-}
-
-describe("Alert Component", () => {
-    beforeAll(() => {
-        globalThis.Notification = myNotification as unknown as jest.Mocked<typeof Notification>;
-    });
-    beforeEach(() => {
-        jest.clearAllMocks();
-    });
-    it("renders", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={defaultAlerts} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.tagName).toBe("DIV");
-    });
-    it("displays a success alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={defaultAlerts} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-success")).toBeInTheDocument();
-    });
-    it("displays an error alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("error")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-error")).toBeInTheDocument();
-    });
-    it("displays a warning alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("warning")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-warning")).toBeInTheDocument();
-    });
-    it("displays an info alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("info")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-info")).toBeInTheDocument();
-    });
-    it("gets favicon URL from document link tags", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        link.href = "/test-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        if (linkElement) {
-            expect(linkElement.getAttribute("href")).toBe("/test-icon.png");
-        } else {
-            expect(true).toBe(false);
-        }
-        document.head.removeChild(link);
-    });
-
-    it("closes alert on close button click", async () => {
-        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const closeButton = await screen.findByRole("button", { name: /close/i });
-        await userEvent.click(closeButton);
-        await waitFor(() => {
-            const alertMessage = screen.queryByText("Test Alert");
-            expect(alertMessage).not.toBeInTheDocument();
-        });
-    });
-
-    it("Alert disappears when alert type is empty", async () => {
-        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
-        const { rerender } = render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        await screen.findByRole("button", { name: /close/i });
-        const newAlerts = [{ atype: "", message: "Test Alert", duration: 3000, system: false }];
-        rerender(
-            <SnackbarProvider>
-                <Alert alerts={newAlerts} />
-            </SnackbarProvider>,
-        );
-        await waitFor(() => {
-            const alertMessage = screen.queryByText("Test Alert");
-            expect(alertMessage).not.toBeInTheDocument();
-        });
+describe("TaipyAlert Component", () => {
+    it("renders with default properties", () => {
+        const { getByRole } = render(<TaipyAlert message="Default Alert" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-filledError");
     });
 
-    it("does nothing when alert is undefined", async () => {
-        render(
-            <SnackbarProvider>
-                <Alert alerts={[]} />
-            </SnackbarProvider>,
-        );
-        expect(Notification.requestPermission).not.toHaveBeenCalled();
+    it("applies the correct severity", () => {
+        const { getByRole } = render(<TaipyAlert message="Warning Alert" severity="warning" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-filledWarning");
     });
 
-    it("validates href when rel attribute is 'icon' and href is set", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        link.href = "/test-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        expect(linkElement?.getAttribute("href")).toBe("/test-icon.png");
-        document.head.removeChild(link);
+    it("applies the correct variant", () => {
+        const { getByRole } = render(<TaipyAlert message="Outlined Alert" variant="outlined" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-outlinedError");
     });
 
-    it("verifies default favicon for 'icon' rel attribute when href is unset/empty", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        expect(linkElement?.getAttribute("href") || "/favicon.png").toBe("/favicon.png");
-        document.head.removeChild(link);
+    it("does not render if render prop is false", () => {
+        const { queryByRole } = render(<TaipyAlert message="Hidden Alert" render={false} />);
+        const alert = queryByRole("alert");
+        expect(alert).toBeNull();
     });
 
-    it("validates href when rel attribute is 'shortcut icon' and href is provided", () => {
-        const link = document.createElement("link");
-        link.rel = "shortcut icon";
-        link.href = "/test-shortcut-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='shortcut icon']");
-        expect(linkElement?.getAttribute("href")).toBe("/test-shortcut-icon.png");
-        document.head.removeChild(link);
+    it("handles dynamic class names", () => {
+        const { getByRole } = render(<TaipyAlert message="Dynamic Alert" className="custom-class" />);
+        const alert = getByRole("alert");
+        expect(alert).toHaveClass("custom-class");
     });
 });

+ 32 - 66
frontend/taipy-gui/src/components/Taipy/Alert.tsx

@@ -11,74 +11,40 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useCallback, useEffect, useMemo, useRef } from "react";
-import { SnackbarKey, useSnackbar, VariantType } from "notistack";
-import IconButton from "@mui/material/IconButton";
-import CloseIcon from "@mui/icons-material/Close";
-
-import { AlertMessage, createDeleteAlertAction } from "../../context/taipyReducers";
-import { useDispatch } from "../../utils/hooks";
-
-interface AlertProps {
-    alerts: AlertMessage[];
+import React from "react";
+import Alert from "@mui/material/Alert";
+import { TaipyBaseProps } from "./utils";
+import { useClassNames, useDynamicProperty } from "../../utils/hooks";
+
+interface AlertProps extends TaipyBaseProps {
+    severity?: "error" | "warning" | "info" | "success";
+    message?: string;
+    variant?: "filled" | "outlined";
+    render?: boolean;
+    defaultMessage?: string;
+    defaultSeverity?: string;
+    defaultVariant?: string;
+    defaultRender?: boolean;
 }
 
-const Alert = ({ alerts }: AlertProps) => {
-    const alert = alerts.length ? alerts[0] : undefined;
-    const lastKey = useRef<SnackbarKey>("");
-    const { enqueueSnackbar, closeSnackbar } = useSnackbar();
-    const dispatch = useDispatch();
-
-    const resetAlert = useCallback(
-        (key: SnackbarKey) => () => {
-            closeSnackbar(key);
-        },
-        [closeSnackbar]
-    );
-
-    const notifAction = useCallback(
-        (key: SnackbarKey) => (
-            <IconButton size="small" aria-label="close" color="inherit" onClick={resetAlert(key)}>
-                <CloseIcon fontSize="small" />
-            </IconButton>
-        ),
-        [resetAlert]
+const TaipyAlert = (props: AlertProps) => {
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+    const render = useDynamicProperty(props.render, props.defaultRender, true);
+    const severity = useDynamicProperty(props.severity, props.defaultSeverity, "error") as
+        | "error"
+        | "warning"
+        | "info"
+        | "success";
+    const variant = useDynamicProperty(props.variant, props.defaultVariant, "filled") as "filled" | "outlined";
+    const message = useDynamicProperty(props.message, props.defaultMessage, "");
+
+    if (!render) return null;
+
+    return (
+        <Alert severity={severity} variant={variant} id={props.id} className={className}>
+            {message}
+        </Alert>
     );
-
-    const faviconUrl = useMemo(() => {
-        const nodeList = document.getElementsByTagName("link");
-        for (let i = 0; i < nodeList.length; i++) {
-            if (nodeList[i].getAttribute("rel") == "icon" || nodeList[i].getAttribute("rel") == "shortcut icon") {
-                return nodeList[i].getAttribute("href") || "/favicon.png";
-            }
-        }
-        return "/favicon.png";
-    }, []);
-
-    useEffect(() => {
-        if (alert) {
-            if (alert.atype === "") {
-                if (lastKey.current) {
-                    closeSnackbar(lastKey.current);
-                    lastKey.current = "";
-                }
-            } else {
-                lastKey.current = enqueueSnackbar(alert.message, {
-                    variant: alert.atype as VariantType,
-                    action: notifAction,
-                    autoHideDuration: alert.duration,
-                });
-                alert.system && new Notification(document.title || "Taipy", { body: alert.message, icon: faviconUrl });
-            }
-            dispatch(createDeleteAlertAction());
-        }
-    }, [alert, enqueueSnackbar, closeSnackbar, notifAction, faviconUrl, dispatch]);
-
-    useEffect(() => {
-        alert?.system && window.Notification && Notification.requestPermission();
-    }, [alert?.system]);
-
-    return null;
 };
 
-export default Alert;
+export default TaipyAlert;

+ 202 - 0
frontend/taipy-gui/src/components/Taipy/Notification.spec.tsx

@@ -0,0 +1,202 @@
+/*
+ * 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 React from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { SnackbarProvider } from "notistack";
+
+import Alert from "./Notification";
+import { AlertMessage } from "../../context/taipyReducers";
+import userEvent from "@testing-library/user-event";
+
+const defaultMessage = "message";
+const defaultAlerts: AlertMessage[] = [{ atype: "success", message: defaultMessage, system: true, duration: 3000 }];
+const getAlertsWithType = (aType: string) => [{ ...defaultAlerts[0], atype: aType }];
+
+class myNotification {
+    static requestPermission = jest.fn(() => Promise.resolve("granted"));
+    static permission = "granted";
+}
+
+describe("Alert Component", () => {
+    beforeAll(() => {
+        globalThis.Notification = myNotification as unknown as jest.Mocked<typeof Notification>;
+    });
+    beforeEach(() => {
+        jest.clearAllMocks();
+    });
+    it("renders", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={defaultAlerts} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.tagName).toBe("DIV");
+    });
+    it("displays a success alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={defaultAlerts} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-success")).toBeInTheDocument();
+    });
+    it("displays an error alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("error")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-error")).toBeInTheDocument();
+    });
+    it("displays a warning alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("warning")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-warning")).toBeInTheDocument();
+    });
+    it("displays an info alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("info")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-info")).toBeInTheDocument();
+    });
+    it("gets favicon URL from document link tags", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        link.href = "/test-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        if (linkElement) {
+            expect(linkElement.getAttribute("href")).toBe("/test-icon.png");
+        } else {
+            expect(true).toBe(false);
+        }
+        document.head.removeChild(link);
+    });
+
+    it("closes alert on close button click", async () => {
+        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const closeButton = await screen.findByRole("button", { name: /close/i });
+        await userEvent.click(closeButton);
+        await waitFor(() => {
+            const alertMessage = screen.queryByText("Test Alert");
+            expect(alertMessage).not.toBeInTheDocument();
+        });
+    });
+
+    it("Alert disappears when alert type is empty", async () => {
+        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false, notificationId: "aNotificationId" }];
+        const { rerender } = render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        await screen.findByRole("button", { name: /close/i });
+        const newAlerts = [{ atype: "", message: "Test Alert", duration: 3000, system: false, notificationId: "aNotificationId" }];
+        rerender(
+            <SnackbarProvider>
+                <Alert alerts={newAlerts} />
+            </SnackbarProvider>,
+        );
+        await waitFor(() => {
+            const alertMessage = screen.queryByText("Test Alert");
+            expect(alertMessage).not.toBeInTheDocument();
+        });
+    });
+
+    it("does nothing when alert is undefined", async () => {
+        render(
+            <SnackbarProvider>
+                <Alert alerts={[]} />
+            </SnackbarProvider>,
+        );
+        expect(Notification.requestPermission).not.toHaveBeenCalled();
+    });
+
+    it("validates href when rel attribute is 'icon' and href is set", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        link.href = "/test-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        expect(linkElement?.getAttribute("href")).toBe("/test-icon.png");
+        document.head.removeChild(link);
+    });
+
+    it("verifies default favicon for 'icon' rel attribute when href is unset/empty", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        expect(linkElement?.getAttribute("href") || "/favicon.png").toBe("/favicon.png");
+        document.head.removeChild(link);
+    });
+
+    it("validates href when rel attribute is 'shortcut icon' and href is provided", () => {
+        const link = document.createElement("link");
+        link.rel = "shortcut icon";
+        link.href = "/test-shortcut-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='shortcut icon']");
+        expect(linkElement?.getAttribute("href")).toBe("/test-shortcut-icon.png");
+        document.head.removeChild(link);
+    });
+});

+ 81 - 0
frontend/taipy-gui/src/components/Taipy/Notification.tsx

@@ -0,0 +1,81 @@
+/*
+ * 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 React, { useCallback, useEffect, useMemo } from "react";
+import { SnackbarKey, useSnackbar, VariantType } from "notistack";
+import IconButton from "@mui/material/IconButton";
+import CloseIcon from "@mui/icons-material/Close";
+
+import { AlertMessage, createDeleteAlertAction } from "../../context/taipyReducers";
+import { useDispatch } from "../../utils/hooks";
+
+interface NotificationProps {
+    alerts: AlertMessage[];
+}
+
+const TaipyNotification = ({ alerts }: NotificationProps) => {
+    const alert = alerts.length ? alerts[0] : undefined;
+    const { enqueueSnackbar, closeSnackbar } = useSnackbar();
+    const dispatch = useDispatch();
+
+    const resetAlert = useCallback(
+        (key: SnackbarKey) => () => {
+            closeSnackbar(key);
+        },
+        [closeSnackbar]
+    );
+
+    const notifAction = useCallback(
+        (key: SnackbarKey) => (
+            <IconButton size="small" aria-label="close" color="inherit" onClick={resetAlert(key)}>
+                <CloseIcon fontSize="small" />
+            </IconButton>
+        ),
+        [resetAlert]
+    );
+
+    const faviconUrl = useMemo(() => {
+        const nodeList = document.getElementsByTagName("link");
+        for (let i = 0; i < nodeList.length; i++) {
+            if (nodeList[i].getAttribute("rel") == "icon" || nodeList[i].getAttribute("rel") == "shortcut icon") {
+                return nodeList[i].getAttribute("href") || "/favicon.png";
+            }
+        }
+        return "/favicon.png";
+    }, []);
+
+    useEffect(() => {
+        if (alert) {
+            const notificationId = alert.notificationId || "";
+            if (alert.atype === "") {
+                closeSnackbar(notificationId);
+            } else {
+                enqueueSnackbar(alert.message, {
+                    variant: alert.atype as VariantType,
+                    action: notifAction,
+                    autoHideDuration: alert.duration,
+                    key: notificationId,
+                });
+                alert.system && new Notification(document.title || "Taipy", { body: alert.message, icon: faviconUrl });
+            }
+            dispatch(createDeleteAlertAction(notificationId));
+        }
+    }, [alert, enqueueSnackbar, closeSnackbar, notifAction, faviconUrl, dispatch]);
+    useEffect(() => {
+        alert?.system && window.Notification && Notification.requestPermission();
+    }, [alert?.system]);
+
+    return null;
+};
+
+export default TaipyNotification;

+ 46 - 3
frontend/taipy-gui/src/components/Taipy/TableFilter.spec.tsx

@@ -12,7 +12,7 @@
  */
 
 import React from "react";
-import { render } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
@@ -48,7 +48,8 @@ beforeEach(() => {
 });
 
 afterEach(() => {
-    // @ts-ignore
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-expect-error
     delete window.matchMedia;
 });
 
@@ -115,7 +116,7 @@ describe("Table Filter Component", () => {
         expect(validate).not.toBeDisabled();
     });
     it("behaves on boolean column", async () => {
-        const { getByTestId, getAllByTestId, findByRole, getByText, getAllByText } = render(
+        const { getByTestId, getAllByTestId, findByRole, getByText } = render(
             <TableFilter columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} filteredCount={0} />
         );
         const elt = getByTestId("FilterListIcon");
@@ -245,3 +246,45 @@ describe("Table Filter Component", () => {
         expect(ddElts2).toHaveLength(2);
     });
 });
+describe("Table Filter Component - Case Insensitive Test", () => {
+    it("renders the case sensitivity toggle switch", async () => {
+        const { getByTestId, getAllByTestId, findByRole, getByText, getAllByText } = render(
+            <TableFilter columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} filteredCount={0} />
+        );
+
+        // Open filter popover
+        const filterIcon = getByTestId("FilterListIcon");
+        await userEvent.click(filterIcon);
+
+        // Select string column from dropdown
+        const dropdownIcons = getAllByTestId("ArrowDropDownIcon");
+        await userEvent.click(dropdownIcons[0].parentElement?.firstElementChild || dropdownIcons[0]);
+        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();
+    });
+});

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

@@ -27,12 +27,14 @@ 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";
 
 import { ColumnDesc, defaultDateFormat, getSortByIndex, iconInRowSx, FilterDesc } from "./tableUtils";
 import { getDateTime, getTypeFromDf } from "../../utils";
 import { getSuffixedClassNames } from "./utils";
+import { MatchCase } from "../icons/MatchCase";
 
 interface TableFilterProps {
     columns: Record<string, ColumnDesc>;
@@ -92,7 +94,13 @@ const getActionsByType = (colType?: string) =>
     (colType && colType in actionsByType && actionsByType[colType]) ||
     (colType === "any" ? { ...actionsByType.string, ...actionsByType.number } : actionsByType.string);
 
-const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?: string, val?: string) => {
+const getFilterDesc = (
+    columns: Record<string, ColumnDesc>,
+    colId?: string,
+    act?: string,
+    val?: string,
+    matchCase?: boolean
+) => {
     if (colId && act && val !== undefined) {
         const colType = getTypeFromDf(columns[colId].type);
         if (val === "" && (colType === "date" || colType === "number" || colType === "boolean")) {
@@ -113,6 +121,7 @@ const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?
                             : val
                         : val,
                 type: colType,
+                matchCase: !!matchCase,
             } as FilterDesc;
         } catch (e) {
             console.info("could not parse value ", val, e);
@@ -126,9 +135,15 @@ const FilterRow = (props: FilterRowProps) => {
     const [colId, setColId] = useState<string>("");
     const [action, setAction] = useState<string>("");
     const [val, setVal] = useState<string>("");
+    const [matchCase, setMatchCase] = useState<boolean>(false);
     const [enableCheck, setEnableCheck] = useState(false);
     const [enableDel, setEnableDel] = useState(false);
 
+    // Function to handle case-sensitivity toggle
+    const toggleMatchCase = useCallback(() => {
+        setMatchCase((prev) => !prev);
+    }, []);
+
     const onColSelect = useCallback(
         (e: SelectChangeEvent<string>) => {
             setColId(e.target.value);
@@ -136,6 +151,7 @@ const FilterRow = (props: FilterRowProps) => {
         },
         [columns, action, val]
     );
+
     const onActSelect = useCallback(
         (e: SelectChangeEvent<string>) => {
             setAction(e.target.value);
@@ -143,6 +159,7 @@ const FilterRow = (props: FilterRowProps) => {
         },
         [columns, colId, val]
     );
+
     const onValueChange = useCallback(
         (e: ChangeEvent<HTMLInputElement>) => {
             setVal(e.target.value);
@@ -150,13 +167,16 @@ const FilterRow = (props: FilterRowProps) => {
         },
         [columns, colId, action]
     );
+
     const onValueAutoComp = useCallback(
         (e: SyntheticEvent, value: string | null) => {
-            setVal(value || "");
-            setEnableCheck(!!getFilterDesc(columns, colId, action, value || ""));
+            const inputValue = value || "";
+            setVal(inputValue);
+            setEnableCheck(!!getFilterDesc(columns, colId, action, inputValue));
         },
         [columns, colId, action]
     );
+
     const onValueSelect = useCallback(
         (e: SelectChangeEvent<string>) => {
             setVal(e.target.value);
@@ -164,6 +184,7 @@ const FilterRow = (props: FilterRowProps) => {
         },
         [columns, colId, action]
     );
+
     const onDateChange = useCallback(
         (v: Date | null) => {
             const dv = !(v instanceof Date) || isNaN(v.valueOf()) ? "" : v.toISOString();
@@ -174,10 +195,11 @@ const FilterRow = (props: FilterRowProps) => {
     );
 
     const onDeleteClick = useCallback(() => setFilter(idx, undefined as unknown as FilterDesc, true), [idx, setFilter]);
+
     const onCheckClick = useCallback(() => {
-        const fd = getFilterDesc(columns, colId, action, val);
+        const fd = getFilterDesc(columns, colId, action, val, matchCase);
         fd && setFilter(idx, fd);
-    }, [idx, setFilter, columns, colId, action, val]);
+    }, [idx, setFilter, columns, colId, action, val, matchCase]);
 
     useEffect(() => {
         if (filter && idx > -1) {
@@ -280,9 +302,24 @@ const FilterRow = (props: FilterRowProps) => {
                         onChange={onValueChange}
                         label={`${val ? "" : "Empty "}String`}
                         margin="dense"
+                        slotProps={{
+                            input: {
+                                endAdornment: (
+                                    <Switch
+                                        onChange={toggleMatchCase}
+                                        checked={matchCase}
+                                        size="small"
+                                        checkedIcon={<MatchCase />}
+                                        icon={<MatchCase color="disabled" />}
+                                        inputProps={{ "aria-label": "Case Sensitive Toggle" }}
+                                    />
+                                ),
+                            },
+                        }}
                     />
                 )}
             </Grid>
+
             <Grid size={1}>
                 <Tooltip title="Validate">
                     <span>

+ 3 - 1
frontend/taipy-gui/src/components/Taipy/index.ts

@@ -40,6 +40,7 @@ import Selector from "./Selector";
 import Slider from "./Slider";
 import StatusList from "./StatusList";
 import Table from "./Table";
+import TaipyAlert from "./Alert";
 import TaipyStyle from "./TaipyStyle";
 import Toggle from "./Toggle";
 import TimeSelector from "./TimeSelector";
@@ -51,6 +52,7 @@ export const getRegisteredComponents = () => {
     if (registeredComponents.TreeView === undefined) {
         Object.entries({
             a: Link,
+            Alert: TaipyAlert,
             Button,
             Chat,
             Chart,
@@ -81,7 +83,7 @@ export const getRegisteredComponents = () => {
             Toggle,
             TreeView,
             Progress,
-        }).forEach(([name, comp]) => (registeredComponents[name] = comp  as ComponentType));
+        }).forEach(([name, comp]) => (registeredComponents[name] = comp as ComponentType));
         if (window.taipyConfig?.extensions) {
             Object.entries(window.taipyConfig.extensions).forEach(([libName, elements]) => {
                 if (elements && elements.length) {

+ 1 - 0
frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

@@ -222,6 +222,7 @@ export interface FilterDesc {
     action: string;
     value: string | number | boolean | Date;
     type: string;
+    matchcase?: boolean;
 }
 
 export const defaultColumns = {} as Record<string, ColumnDesc>;

+ 23 - 0
frontend/taipy-gui/src/components/icons/MatchCase.tsx

@@ -0,0 +1,23 @@
+/*
+ * 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 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>
+);

+ 32 - 6
frontend/taipy-gui/src/context/taipyReducers.spec.ts

@@ -50,6 +50,7 @@ import { Socket } from "socket.io-client";
 import { Dispatch } from "react";
 import { parseData } from "../utils/dataFormat";
 import * as wsUtils from "./wsUtils";
+import { nanoid } from 'nanoid';
 
 jest.mock("./utils", () => ({
     ...jest.requireActual("./utils"),
@@ -575,6 +576,7 @@ describe("taipyReducer function", () => {
             message: "some error message",
             system: true,
             duration: 3000,
+            notificationId: nanoid(),
         };
         const newState = taipyReducer({ ...INITIAL_STATE }, action);
         expect(newState.alerts).toContainEqual({
@@ -582,19 +584,37 @@ describe("taipyReducer function", () => {
             message: action.message,
             system: action.system,
             duration: action.duration,
+            notificationId: action.notificationId,
         });
     });
     it("should handle DELETE_ALERT action", () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
         const initialState = {
             ...INITIAL_STATE,
             alerts: [
-                { atype: "error", message: "First Alert", system: true, duration: 5000 },
-                { atype: "warning", message: "Second Alert", system: false, duration: 3000 },
+                { atype: "error", message: "First Alert", system: true, duration: 5000, notificationId: notificationId1 },
+                { atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 },
             ],
         };
-        const action = { type: Types.DeleteAlert };
+        const action = { type: Types.DeleteAlert, notificationId: notificationId1 };
+        const newState = taipyReducer(initialState, action);
+        expect(newState.alerts).toEqual([{ atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 }]);
+    });
+    it('should not modify state if DELETE_ALERT does not match any notificationId', () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
+        const nonExistentId = "000000";
+        const initialState = {
+            ...INITIAL_STATE,
+            alerts: [
+                { atype: "error", message: "First Alert", system: true, duration: 5000, notificationId: notificationId1 },
+                { atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 },
+            ],
+        };
+        const action = { type: Types.DeleteAlert, notificationId: nonExistentId };
         const newState = taipyReducer(initialState, action);
-        expect(newState.alerts).toEqual([{ atype: "warning", message: "Second Alert", system: false, duration: 3000 }]);
+        expect(newState).toEqual(initialState);
     });
     it("should not modify state if no alerts are present", () => {
         const initialState = { ...INITIAL_STATE, alerts: [] };
@@ -602,7 +622,10 @@ describe("taipyReducer function", () => {
         const newState = taipyReducer(initialState, action);
         expect(newState).toEqual(initialState);
     });
-    it("should handle DELETE_ALERT action", () => {
+    it("should handle DELETE_ALERT action even when no notificationId is passed", () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
+
         const initialState = {
             ...INITIAL_STATE,
             alerts: [
@@ -611,16 +634,18 @@ describe("taipyReducer function", () => {
                     atype: "type1",
                     system: true,
                     duration: 5000,
+                    notificationId: notificationId1,
                 },
                 {
                     message: "alert2",
                     atype: "type2",
                     system: false,
                     duration: 3000,
+                    notificationId: notificationId2,
                 },
             ],
         };
-        const action = { type: Types.DeleteAlert };
+        const action = { type: Types.DeleteAlert, notificationId: notificationId1 };
         const newState = taipyReducer(initialState, action);
         expect(newState.alerts).toEqual([
             {
@@ -628,6 +653,7 @@ describe("taipyReducer function", () => {
                 atype: "type2",
                 system: false,
                 duration: 3000,
+                notificationId: notificationId2,
             },
         ]);
     });

+ 15 - 5
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -16,6 +16,7 @@ import { createTheme, Theme } from "@mui/material/styles";
 import merge from "lodash/merge";
 import { Dispatch } from "react";
 import { io, Socket } from "socket.io-client";
+import { nanoid } from 'nanoid';
 
 import { FilterDesc } from "../components/Taipy/tableUtils";
 import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
@@ -91,6 +92,7 @@ export interface AlertMessage {
     message: string;
     system: boolean;
     duration: number;
+    notificationId?: string;
 }
 
 interface TaipyAction extends NamePayload, TaipyBaseAction {
@@ -108,6 +110,10 @@ interface TaipyMultipleMessageAction extends TaipyBaseAction {
 
 interface TaipyAlertAction extends TaipyBaseAction, AlertMessage {}
 
+interface TaipyDeleteAlertAction extends TaipyBaseAction {
+    notificationId: string;
+}
+
 export const BLOCK_CLOSE = { action: "", message: "", close: true, noCancel: false } as BlockMessage;
 
 export interface BlockMessage {
@@ -379,14 +385,16 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta
                         message: alertAction.message,
                         system: alertAction.system,
                         duration: alertAction.duration,
+                        notificationId: alertAction.notificationId || nanoid(),
                     },
                 ],
             };
         case Types.DeleteAlert:
-            if (state.alerts.length) {
-                return { ...state, alerts: state.alerts.filter((_, i) => i) };
-            }
-            return state;
+            const deleteAlertAction = action as unknown as TaipyAlertAction;
+            return {
+                ...state,
+                alerts: state.alerts.filter(alert => alert.notificationId !== deleteAlertAction.notificationId),
+            };
         case Types.SetBlock:
             const blockAction = action as unknown as TaipyBlockAction;
             if (blockAction.close) {
@@ -818,10 +826,12 @@ export const createAlertAction = (alert: AlertMessage): TaipyAlertAction => ({
     message: alert.message,
     system: alert.system,
     duration: alert.duration,
+    notificationId: alert.notificationId,
 });
 
-export const createDeleteAlertAction = (): TaipyBaseAction => ({
+export const createDeleteAlertAction = (notificationId: string): TaipyDeleteAlertAction => ({
     type: Types.DeleteAlert,
+    notificationId,
 });
 
 export const createBlockAction = (block: BlockMessage): TaipyBlockAction => ({

+ 53 - 11
taipy/gui/_gui_cli.py

@@ -43,7 +43,7 @@ class _GuiCLI(_AbstractCLI):
             "nargs": "?",
             "default": "",
             "const": "",
-            "help": "Specify client url",
+            "help": "Specify client URL",
         },
         ("--ngrok-token",): {
             "dest": "taipy_ngrok_token",
@@ -81,21 +81,55 @@ class _GuiCLI(_AbstractCLI):
         "--no-reloader": {"dest": "taipy_no_reloader", "help": "No reload on code changes", "action": "store_true"},
     }
 
+    __BROWSER_ARGS: Dict[str, Dict] = {
+        "--run-browser": {
+            "dest": "taipy_run_browser",
+            "help": "Open a new tab in the system browser",
+            "action": "store_true",
+        },
+        "--no-run-browser": {
+            "dest": "taipy_no_run_browser",
+            "help": "Don't open a new tab for the application",
+            "action": "store_true",
+        },
+    }
+
+    __DARK_LIGHT_MODE_ARGS: Dict[str, Dict] = {
+        "--dark-mode": {
+            "dest": "taipy_dark_mode",
+            "help": "Apply dark mode to the GUI application",
+            "action": "store_true",
+        },
+        "--light-mode": {
+            "dest": "taipy_light_mode",
+            "help": "Apply light mode to the GUI application",
+            "action": "store_true",
+        },
+    }
+
     @classmethod
     def create_parser(cls):
         gui_parser = _TaipyParser._add_groupparser("Taipy GUI", "Optional arguments for Taipy GUI service")
 
         for args, arg_dict in cls.__GUI_ARGS.items():
-            taipy_arg = (args[0], cls.__add_taipy_prefix(args[0]), *args[1:])
-            gui_parser.add_argument(*taipy_arg, **arg_dict)
+            arg = (args[0], cls.__add_taipy_prefix(args[0]), *args[1:])
+            gui_parser.add_argument(*arg, **arg_dict)
 
         debug_group = gui_parser.add_mutually_exclusive_group()
-        for debug_arg, debug_arg_dict in cls.__DEBUG_ARGS.items():
-            debug_group.add_argument(debug_arg, cls.__add_taipy_prefix(debug_arg), **debug_arg_dict)
+        for arg, arg_dict in cls.__DEBUG_ARGS.items():
+            debug_group.add_argument(arg, cls.__add_taipy_prefix(arg), **arg_dict)
 
         reloader_group = gui_parser.add_mutually_exclusive_group()
-        for reloader_arg, reloader_arg_dict in cls.__RELOADER_ARGS.items():
-            reloader_group.add_argument(reloader_arg, cls.__add_taipy_prefix(reloader_arg), **reloader_arg_dict)
+        for arg, arg_dict in cls.__RELOADER_ARGS.items():
+            reloader_group.add_argument(arg, cls.__add_taipy_prefix(arg), **arg_dict)
+
+        browser_group = gui_parser.add_mutually_exclusive_group()
+        for arg, arg_dict in cls.__BROWSER_ARGS.items():
+            browser_group.add_argument(arg, cls.__add_taipy_prefix(arg), **arg_dict)
+
+        dark_light_mode_group = gui_parser.add_mutually_exclusive_group()
+        for arg, arg_dict in cls.__DARK_LIGHT_MODE_ARGS.items():
+            dark_light_mode_group.add_argument(arg, cls.__add_taipy_prefix(arg), **arg_dict)
 
         if (hook_cli_arg := _Hooks()._get_cli_args()) is not None:
             hook_group = gui_parser.add_mutually_exclusive_group()
@@ -109,12 +143,20 @@ class _GuiCLI(_AbstractCLI):
             run_parser.add_argument(*args, **arg_dict)
 
         debug_group = run_parser.add_mutually_exclusive_group()
-        for debug_arg, debug_arg_dict in cls.__DEBUG_ARGS.items():
-            debug_group.add_argument(debug_arg, **debug_arg_dict)
+        for arg, arg_dict in cls.__DEBUG_ARGS.items():
+            debug_group.add_argument(arg, **arg_dict)
 
         reloader_group = run_parser.add_mutually_exclusive_group()
-        for reloader_arg, reloader_arg_dict in cls.__RELOADER_ARGS.items():
-            reloader_group.add_argument(reloader_arg, **reloader_arg_dict)
+        for arg, arg_dict in cls.__RELOADER_ARGS.items():
+            reloader_group.add_argument(arg, **arg_dict)
+
+        browser_group = run_parser.add_mutually_exclusive_group()
+        for arg, arg_dict in cls.__BROWSER_ARGS.items():
+            browser_group.add_argument(arg, **arg_dict)
+
+        dark_light_mode_group = run_parser.add_mutually_exclusive_group()
+        for arg, arg_dict in cls.__DARK_LIGHT_MODE_ARGS.items():
+            dark_light_mode_group.add_argument(arg, **arg_dict)
 
         if (hook_cli_arg := _Hooks()._get_cli_args()) is not None:
             hook_group = run_parser.add_mutually_exclusive_group()

+ 11 - 1
taipy/gui/_page.py

@@ -17,6 +17,8 @@ import re
 import typing as t
 import warnings
 
+from ._warnings import TaipyGuiAlwaysWarning
+
 if t.TYPE_CHECKING:
     from ._renderers import Page
     from .gui import Gui
@@ -40,7 +42,15 @@ class _Page(object):
             warnings.resetwarnings()
             with gui._set_locals_context(self._renderer._get_module_name()):
                 self._rendered_jsx = self._renderer.render(gui)
-            if not silent:
+            if silent:
+                s = ""
+                for wm in w:
+                    if wm.category is TaipyGuiAlwaysWarning:
+                        s += f" - {wm.message}\n"
+                if s:
+                    logging.warning("\033[1;31m\n" + s)
+
+            else:
                 if (
                     self._rendered_jsx
                     and isinstance(self._rendered_jsx, str)

+ 16 - 0
taipy/gui/_renderers/factory.py

@@ -30,6 +30,7 @@ class _Factory:
     __TAIPY_NAME_SPACE = "taipy."
 
     __CONTROL_DEFAULT_PROP_NAME = {
+        "alert": "message",
         "button": "label",
         "chat": "messages",
         "chart": "data",
@@ -70,6 +71,21 @@ class _Factory:
     __LIBRARIES: t.Dict[str, t.List["ElementLibrary"]] = {}
 
     __CONTROL_BUILDERS = {
+        "alert":
+        lambda gui, control_type, attrs: _Builder(
+            gui=gui,
+            control_type=control_type,
+            element_name="Alert",
+            attributes=attrs,
+        )
+        .set_value_and_default(var_type=PropertyType.dynamic_string)
+        .set_attributes(
+            [
+                ("severity", PropertyType.dynamic_string),
+                ("variant", PropertyType.dynamic_string),
+                ("render", PropertyType.dynamic_boolean, True),
+            ]
+        ),
         "button": lambda gui, control_type, attrs: _Builder(
             gui=gui,
             control_type=control_type,

+ 13 - 4
taipy/gui/_warnings.py

@@ -30,15 +30,24 @@ class TaipyGuiWarning(UserWarning):
         )
 
 
-def _warn(message: str, e: t.Optional[BaseException] = None):
+class TaipyGuiAlwaysWarning(TaipyGuiWarning):
+    pass
+
+
+def _warn(
+    message: str,
+    e: t.Optional[BaseException] = None,
+    always_show: t.Optional[bool] = False,
+):
     warnings.warn(
         (
-            f"{message}:\n{''.join(traceback.format_exception(type(e), e, e.__traceback__))}"
+            f"{message}:\n{''.join(traceback.format_exception(e))}"
             if e and TaipyGuiWarning._tp_debug_mode
-            else f"{message}:\n{e}"
+            else f"{message}:\n"
+            + "".join(traceback.format_exception(None, e, e.__traceback__.tb_next if e.__traceback__ else None))
             if e
             else message
         ),
-        TaipyGuiWarning,
+        TaipyGuiWarning if not always_show else TaipyGuiAlwaysWarning,
         stacklevel=2,
     )

+ 7 - 1
taipy/gui/config.py

@@ -214,6 +214,12 @@ class _Config(object):
             config["use_reloader"] = True
         if args.taipy_no_reloader:
             config["use_reloader"] = False
+        if args.taipy_run_browser:
+            config["run_browser"] = True
+        if args.taipy_no_run_browser:
+            config["run_browser"] = False
+        if args.taipy_dark_mode or args.taipy_light_mode:
+            config["dark_mode"] = not args.taipy_light_mode
         if args.taipy_ngrok_token:
             config["ngrok_token"] = args.taipy_ngrok_token
         if args.taipy_webapp_path:
@@ -250,7 +256,7 @@ class _Config(object):
                         config[key] = value if config.get(key) is None else type(config.get(key))(value)  # type: ignore[reportCallIssue]
                 except Exception as e:
                     _warn(
-                        f"Invalid keyword arguments value in Gui.run {key} - {value}. Unable to parse value to the correct type",  # noqa: E501
+                        f"Invalid keyword arguments value in Gui.run(): {key} - {value}. Unable to parse value to the correct type",  # noqa: E501
                         e,
                     )
         # Load config from env file

+ 24 - 5
taipy/gui/data/pandas_data_accessor.py

@@ -267,18 +267,37 @@ class _PandasDataAccessor(_DataAccessor):
                 col = fd.get("col")
                 val = fd.get("value")
                 action = fd.get("action")
+                match_case = fd.get("matchCase", False) is not False  # Ensure it's a boolean
+                right = None
+                col_expr = f"`{col}`"
+
                 if isinstance(val, str):
                     if self.__is_date_column(t.cast(pd.DataFrame, df), col):
                         val = datetime.fromisoformat(val[:-1])
+                    elif not match_case:
+                        if action != "contains":
+                            col_expr = f"{col_expr}.str.lower()"
+                        val = val.lower()
+                    vars.append(val)
+                    val_var = f"@vars[{len(vars) - 1}]"
+                    if action == "contains":
+                        right = f".str.contains({val_var}{'' if match_case else ', case=False'})"
+                else:
                     vars.append(val)
-                val = f"@vars[{len(vars) - 1}]" if isinstance(val, (str, datetime)) else val
-                right = f".str.contains({val})" if action == "contains" else f" {action} {val}"
+                    val_var = f"@vars[{len(vars) - 1}]"
+
+                if right is None:
+                    right = f" {action} {val_var}"
+
                 if query:
                     query += " and "
-                query += f"`{col}`{right}"
+                query += f"{col_expr}{right}"
+
+            # Apply filters using df.query()
             try:
-                df = df.query(query)
-                is_copied = True
+                if query:
+                    df = df.query(query)
+                    is_copied = True
             except Exception as e:
                 _warn(f"Dataframe filtering: invalid query '{query}' on {df.head()}", e)
 

+ 40 - 8
taipy/gui/gui.py

@@ -21,6 +21,7 @@ import sys
 import tempfile
 import time
 import typing as t
+import uuid
 import warnings
 from importlib import metadata, util
 from importlib.util import find_spec
@@ -1330,15 +1331,26 @@ class Gui:
             send_back_only=True,
         )
 
-    def __send_ws_alert(self, type: str, message: str, system_notification: bool, duration: int) -> None:
+    def __send_ws_alert(
+            self, type: str,
+            message: str,
+            system_notification: bool,
+            duration: int,
+            notification_id: t.Optional[str] = None
+        ) -> None:
+        payload = {
+            "type": _WsType.ALERT.value,
+            "atype": type,
+            "message": message,
+            "system": system_notification,
+            "duration": duration,
+        }
+
+        if notification_id:
+            payload["notificationId"] = notification_id
+
         self.__send_ws(
-            {
-                "type": _WsType.ALERT.value,
-                "atype": type,
-                "message": message,
-                "system": system_notification,
-                "duration": duration,
-            }
+            payload,
         )
 
     def __send_ws_partial(self, partial: str):
@@ -2242,13 +2254,33 @@ class Gui:
         message: str = "",
         system_notification: t.Optional[bool] = None,
         duration: t.Optional[int] = None,
+        notification_id: t.Optional[str] = None,
     ):
+        if not notification_id:
+            notification_id = str(uuid.uuid4())
+
         self.__send_ws_alert(
             notification_type,
             message,
             self._get_config("system_notification", False) if system_notification is None else system_notification,
             self._get_config("notification_duration", 3000) if duration is None else duration,
+            notification_id,
         )
+        return notification_id
+
+    def _close_notification(
+        self,
+        notification_id: str,
+    ):
+        if notification_id:
+            self.__send_ws_alert(
+                type="",  # Since you're closing, set type to an empty string or a predefined "close" type
+                message="",  # No need for a message when closing
+                system_notification=False,  # System notification not needed for closing
+                duration=0,  # No duration since it's an immediate close
+                notification_id=notification_id
+            )
+
 
     def _hold_actions(
         self,

+ 11 - 1
taipy/gui/gui_actions.py

@@ -67,6 +67,7 @@ def notify(
     message: str = "",
     system_notification: t.Optional[bool] = None,
     duration: t.Optional[int] = None,
+    notification_id: str = "",
 ):
     """Send a notification to the user interface.
 
@@ -96,11 +97,20 @@ def notify(
     feature.
     """
     if state and isinstance(state._gui, Gui):
-        state._gui._notify(notification_type, message, system_notification, duration)
+        return state._gui._notify(notification_type, message, system_notification, duration, notification_id)
     else:
         _warn("'notify()' must be called in the context of a callback.")
 
 
+def close_notification(state: State, notification_id: str):
+    """Close a specific notification by ID."""
+    if state and isinstance(state._gui, Gui):
+        # Send the close command with the notification_id
+        state._gui._close_notification(notification_id)
+    else:
+        _warn("'close_notification()' must be called in the context of a callback.")
+
+
 def hold_control(
     state: State,
     callback: t.Optional[t.Union[str, t.Callable]] = None,

+ 13 - 3
taipy/gui/utils/_evaluator.py

@@ -47,6 +47,7 @@ class _Evaluator:
     __EXPR_EDGE_CASE_F_STRING = re.compile(r"[\{]*[a-zA-Z_][a-zA-Z0-9_]*:.+")
     __IS_TAIPY_EXPR_RE = re.compile(r"TpExPr_(.*)")
     __IS_ARRAY_EXPR_RE = re.compile(r"[^[]*\[(\d+)][^]]*")
+    __CLEAN_LAMBDA_RE = re.compile(r"^__lambda_[\d_]+(TPMDL_\d+)?(.*)$")
 
     def __init__(self, default_bindings: t.Dict[str, t.Any], shared_variable: t.List[str]) -> None:
         # key = expression, value = hashed value of the expression
@@ -260,7 +261,12 @@ class _Evaluator:
             with gui._get_authorization():
                 expr_evaluated = eval(not_encoded_expr if is_edge_case else expr_string, ctx)
         except Exception as e:
-            _warn(f"Cannot evaluate expression '{not_encoded_expr if is_edge_case else expr_string}'", e)
+            exception_str = not_encoded_expr if is_edge_case else expr_string
+            _warn(
+                f"Cannot evaluate expression '{_Evaluator._clean_exception_expr(exception_str)}'",
+                e,
+                always_show=True,
+            )
             expr_evaluated = None
         if lambda_expr and callable(expr_evaluated):
             expr_hash = _get_lambda_id(expr_evaluated, module=module_name)  # type: ignore[reportArgumentType]
@@ -291,7 +297,7 @@ class _Evaluator:
             if holder is not None:
                 holder.set(expr_evaluated)
         except Exception as e:
-            _warn(f"Exception raised evaluating {expr_string}", e)
+            _warn(f"Exception raised evaluating {_Evaluator._clean_exception_expr(expr_string)}", e)
 
     def re_evaluate_expr(self, gui: Gui, var_name: str) -> t.Set[str]:  # noqa C901
         """
@@ -366,7 +372,7 @@ class _Evaluator:
                         expr_evaluated = eval(expr_string, ctx)
                         _setscopeattr(gui, hash_expr, expr_evaluated)
                     except Exception as e:
-                        _warn(f"Exception raised evaluating {expr_string}", e)
+                        _warn(f"Exception raised evaluating {_Evaluator._clean_exception_expr(expr_string)}", e)
             # refresh holders if any
             for h in self.__expr_to_holders.get(expr, []):
                 holder_hash = self.__get_holder_hash(h, self.get_hash_from_expr(expr))
@@ -378,3 +384,7 @@ class _Evaluator:
 
     def _get_instance_in_context(self, name: str):
         return self.__global_ctx.get(name)
+
+    @staticmethod
+    def _clean_exception_expr(expr: str):
+        return _Evaluator.__CLEAN_LAMBDA_RE.sub(r"<lambda>\2", expr)

+ 34 - 1
taipy/gui/viselements.json

@@ -1625,6 +1625,39 @@
                 ]
             }
         ],
+        [
+            "alert",  
+            {
+                "inherits": ["shared"],
+                "properties": [
+                    {
+                        "name": "message",
+                        "default_property": true,
+                        "type": "dynamic(str)",
+                        "default_value": "\"\"",
+                        "doc": "The message displayed in the notification. Can be a dynamic string."
+                    },
+                    {
+                        "name": "severity",
+                        "type": "dynamic(str)",
+                        "default_value": "\"error\"",
+                        "doc": "The severity level of the alert. Valid values: \"error\", \"warning\", \"info\", \"success\".\nThe default is \"error\"."
+                    },
+                    {
+                        "name": "variant",
+                        "type": "dynamic(str)",
+                        "default_value": "\"filled\"",
+                        "doc": "The variant of the alert. Valid values: \"filled\", \"outlined\".\nThe default is \"filled\"."
+                    },
+                    {
+                        "name": "render",
+                        "type": "dynamic(bool)",
+                        "default_value": "True",
+                        "doc": "If False, the alert is hidden."
+                    }
+                ]
+            }                       
+        ],
         [
             "status",
             {
@@ -1804,7 +1837,7 @@
                     }
                 ]
             }
-        ]
+        ]                                       
     ],
     "blocks": [
         [

+ 5 - 5
taipy/gui_core/viselements.json

@@ -100,7 +100,7 @@
                     },
                     {
                         "name": "filter",
-                        "type": "bool|str|ScenarioFilter|list[str|ScenarioFilter]",
+                        "type": "bool|str|taipy.gui_core.filters.ScenarioFilter|list[str|taipy.gui_core.filters.ScenarioFilter]",
                         "default_value": "\"*\"",
                         "doc": "TODO: a list of <code>Scenario^</code> attributes to filter on. If False, do not allow filter."
                     },
@@ -112,7 +112,7 @@
                     },
                     {
                         "name": "sort",
-                        "type": "bool|str|ScenarioFilter|list[str|ScenarioFilter]",
+                        "type": "bool|str|taipy.gui_core.filters.ScenarioFilter|list[str|taipy.gui_core.filters.ScenarioFilter]",
                         "default_value": "\"*\"",
                         "doc": "TODO: a list of <code>Scenario^</code> attributes to sort on. If False, do not allow sort."
                     }
@@ -355,7 +355,7 @@
                     },
                     {
                         "name": "filter",
-                        "type": "bool|str|DataNodeFilter|list[str|DataNodeFilter]",
+                        "type": "bool|str|taipy.gui_core.filters.DataNodeFilter|list[str|taipy.gui_core.filters.DataNodeFilter]",
                         "default_value": "\"*\"",
                         "doc": "TODO: a list of <code>DataNode^</code> attributes to filter on. If False, do not allow filter."
                     },
@@ -367,7 +367,7 @@
                     },
                     {
                         "name": "sort",
-                        "type": "bool|str|DataNodeFilter|list[str|DataNodeFilter]",
+                        "type": "bool|str|taipy.gui_core.filters.DataNodeFilter|list[str|taipy.gui_core.filters.DataNodeFilter]",
                         "default_value": "\"*\"",
                         "doc": "TODO: a list of <code>DataNode^</code> attributes to sort on. If False, do not allow sort."
                     }
@@ -548,7 +548,7 @@
                     },
                     {
                         "name": "on_details",
-                        "type": "Union[str, Callback, bool]",
+                        "type": "Union[str, Callable, bool]",
                         "doc": "The name of a function that is triggered when the details icon is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the id of the control.</li>\n<li>payload (<code>dict</code>): a dictionary that contains the Job Id in the value for key <i>args<i>.</li>\n</ul></br>If False, the icon is not shown.",
                         "signature": [
                             [

+ 75 - 0
tests/gui/data/test_pandas_data_accessor.py

@@ -13,8 +13,11 @@ import inspect
 import os
 from datetime import datetime
 from importlib import util
+from unittest.mock import Mock
 
 import pandas
+import pandas as pd
+import pytest
 from flask import g
 
 from taipy.gui import Gui
@@ -23,6 +26,26 @@ from taipy.gui.data.decimator import ScatterDecimator
 from taipy.gui.data.pandas_data_accessor import _PandasDataAccessor
 
 
+# Define a mock to simulate _DataFormat behavior with a 'value' attribute
+class MockDataFormat:
+    LIST = Mock(value="list")
+    CSV = Mock(value="csv")
+
+@pytest.fixture
+def pandas_accessor():
+    gui = Mock()
+    return _PandasDataAccessor(gui=gui)
+
+@pytest.fixture
+def sample_df():
+    data = {
+        "StringCol": ["Apple", "Banana", "Cherry", "apple"],
+        "NumberCol": [10, 20, 30, 40],
+        "BoolCol": [True, False, True, False],
+        "DateCol": pd.to_datetime(["2020-01-01", "2021-06-15", "2022-08-22", "2023-03-05"])
+    }
+    return pd.DataFrame(data)
+
 def test_simple_data(gui: Gui, helpers, small_dataframe):
     accessor = _PandasDataAccessor(gui)
     pd = pandas.DataFrame(data=small_dataframe)
@@ -255,6 +278,58 @@ def test_filter_by_date(gui: Gui, helpers, small_dataframe):
     value = accessor.get_data("x", pd, query, _DataFormat.JSON)
     assert len(value["value"]["data"]) == 1
 
+def test_contains_case_sensitive(pandas_accessor, sample_df):
+    payload = {
+        "filters": [{"col": "StringCol", "value": "Apple", "action": "contains", "matchCase": True}]
+    }
+    result = pandas_accessor.get_data("test_var", sample_df, payload, MockDataFormat.LIST)
+    filtered_data = pd.DataFrame(result['value']['data'])
+
+    assert len(filtered_data) == 1
+    assert filtered_data.iloc[0]['StringCol'] == 'Apple'
+
+def test_contains_case_insensitive(pandas_accessor, sample_df):
+    payload = {
+        "filters": [{"col": "StringCol", "value": "apple", "action": "contains", "matchCase": False}]
+    }
+    result = pandas_accessor.get_data("test_var", sample_df, payload, MockDataFormat.LIST)
+    filtered_data = pd.DataFrame(result['value']['data'])
+
+    assert len(filtered_data) == 2
+    assert 'Apple' in filtered_data['StringCol'].values
+    assert 'apple' in filtered_data['StringCol'].values
+
+def test_equals_case_sensitive(pandas_accessor, sample_df):
+    payload = {
+        "filters": [{"col": "StringCol", "value": "Apple", "action": "==", "matchCase": True}]
+    }
+    result = pandas_accessor.get_data("test_var", sample_df, payload, MockDataFormat.LIST)
+    filtered_data = pd.DataFrame(result['value']['data'])
+
+    assert len(filtered_data) == 1
+    assert filtered_data.iloc[0]['StringCol'] == 'Apple'
+
+def test_equals_case_insensitive(pandas_accessor, sample_df):
+    payload = {
+        "filters": [{"col": "StringCol", "value": "apple", "action": "==", "matchCase": False}]
+    }
+    result = pandas_accessor.get_data("test_var", sample_df, payload, MockDataFormat.LIST)
+    filtered_data = pd.DataFrame(result['value']['data'])
+
+    assert len(filtered_data) == 2
+    assert 'Apple' in filtered_data['StringCol'].values
+    assert 'apple' in filtered_data['StringCol'].values
+
+def test_not_equals_case_insensitive(pandas_accessor, sample_df):
+    payload = {
+        "filters": [{"col": "StringCol", "value": "apple", "action": "!=", "matchCase": False}]
+    }
+    result = pandas_accessor.get_data("test_var", sample_df, payload, MockDataFormat.LIST)
+    filtered_data = pd.DataFrame(result['value']['data'])
+
+    assert len(filtered_data) == 2
+    assert 'Banana' in filtered_data['StringCol'].values
+    assert 'Cherry' in filtered_data['StringCol'].values
 
 def test_decimator(gui: Gui, helpers, small_dataframe):
     a_decimator = ScatterDecimator(threshold=1)  # noqa: F841

+ 24 - 0
tests/gui/gui_specific/test_cli.py

@@ -80,6 +80,30 @@ def test_taipy_no_reload(gui: Gui):
         assert gui._config.config.get("use_reloader") is False
 
 
+def test_taipy_run_browser(gui: Gui):
+    with patch("sys.argv", ["prog", "--run-browser"]):
+        gui.run(run_server=False, use_reloader=False)
+        assert gui._config.config.get("run_browser") is True
+
+
+def test_taipy_no_run_browser(gui: Gui):
+    with patch("sys.argv", ["prog", "--no-run-browser"]):
+        gui.run(run_server=False, use_reloader=True)
+        assert gui._config.config.get("run_browser") is False
+
+
+def test_taipy_dark_mode(gui: Gui):
+    with patch("sys.argv", ["prog", "--dark-mode"]):
+        gui.run(run_server=False)
+        assert gui._config.config.get("dark_mode") is True
+
+
+def test_taipy_light_mode(gui: Gui):
+    with patch("sys.argv", ["prog", "--light-mode"]):
+        gui.run(run_server=False)
+        assert gui._config.config.get("dark_mode") is False
+
+
 def test_ngrok_token(gui: Gui):
     with patch("sys.argv", ["prog", "--ngrok-token", "token"]):
         gui.run(run_server=False)

+ 1 - 1
tests/gui/helpers.py

@@ -165,4 +165,4 @@ class Helpers:
 
     @staticmethod
     def get_taipy_warnings(warns: t.List[warnings.WarningMessage]) -> t.List[warnings.WarningMessage]:
-        return [w for w in warns if w.category is TaipyGuiWarning]
+        return [w for w in warns if issubclass(w.category, TaipyGuiWarning)]

+ 90 - 53
tools/gui/generate_pyi.py

@@ -71,27 +71,44 @@ taipy_doc_url = f"https://docs.taipy.io/en/{current_version}/manuals/userman/gui
 
 builder_py_file = "./taipy/gui/builder/__init__.py"
 builder_pyi_file = f"{builder_py_file}i"
+controls: Dict[str, List] = {}
+blocks: Dict[str, List] = {}
+undocumented: Dict[str, List] = {}
 with open("./taipy/gui/viselements.json", "r") as file:
-    viselements = json.load(file)
+    viselements: Dict[str, List] = json.load(file)
+    controls[""] = viselements.get("controls", [])
+    blocks[""] = viselements.get("blocks", [])
+    undocumented[""] = viselements.get("undocumented", [])
+with open("./taipy/gui_core/viselements.json", "r") as file:
+    core_viselements: Dict[str, List] = json.load(file)
+    controls['if find_spec("taipy.core"):'] = core_viselements.get("controls", [])
+    blocks['if find_spec("taipy.core"):'] = core_viselements.get("blocks", [])
+    undocumented['if find_spec("taipy.core"):'] = core_viselements.get("undocumented", [])
+
 os.system(f"pipenv run stubgen {builder_py_file} --no-import --parse-only --export-less -o ./")
 
 with open(builder_pyi_file, "a") as file:
     file.write("from datetime import datetime\n")
+    file.write("from importlib.util import find_spec\n")
     file.write("from typing import Any, Callable, Optional, Union\n")
     file.write("\n")
     file.write("from .. import Icon\n")
     file.write("from ._element import _Block, _Control\n")
+    file.write('if find_spec("taipy.core"):\n')
+    file.write("\tfrom taipy.core import Cycle, DataNode, Job, Scenario\n")
 
 
-def resolve_inherit(name: str, properties, inherits, viselements) -> List[Dict[str, Any]]:
+def resolve_inherit(
+    name: str, properties, inherits, blocks: List, controls: List, undocumented: List
+) -> List[Dict[str, Any]]:
     if not inherits:
         return properties
     for inherit_name in inherits:
-        inherited_desc = next((e for e in viselements["undocumented"] if e[0] == inherit_name), None)
+        inherited_desc = next((e for e in undocumented if e[0] == inherit_name), None)
         if inherited_desc is None:
-            inherited_desc = next((e for e in viselements["blocks"] if e[0] == inherit_name), None)
+            inherited_desc = next((e for e in blocks if e[0] == inherit_name), None)
         if inherited_desc is None:
-            inherited_desc = next((e for e in viselements["controls"] if e[0] == inherit_name), None)
+            inherited_desc = next((e for e in controls if e[0] == inherit_name), None)
         if inherited_desc is None:
             raise RuntimeError(f"Element type '{name}' inherits from unknown element type '{inherit_name}'")
         inherited_desc = inherited_desc[1]
@@ -109,11 +126,13 @@ def resolve_inherit(name: str, properties, inherits, viselements) -> List[Dict[s
                 override(prop_desc, inherit_prop, "signature")
             else:
                 properties.append(inherit_prop)
-            properties = resolve_inherit(inherit_name, properties, inherited_desc.get("inherits", None), viselements)
+            properties = resolve_inherit(
+                inherit_name, properties, inherited_desc.get("inherits", None), blocks, controls, undocumented
+            )
     return properties
 
 
-def format_as_parameter(property):
+def format_as_parameter(property: Dict[str, str]):
     name = property["name"]
     if match := __RE_INDEXED_PROPERTY.match(name):
         name = f"{match.group(1)}__{match.group(3)}"
@@ -130,8 +149,8 @@ def format_as_parameter(property):
         property["dynamic"] = ""
     if type == "Callback" or type == "Function":
         type = "Callable"
-    elif re.match(r"plotly\.", type) or re.match(r"taipy\.", type):
-        type = f"\"{type}\""
+    else:
+        type = re.sub(r"((plotly|taipy)\.[\w\.]*)", r'"\1"', type)
     default_value = property.get("default_value", None)
     if default_value is None or default_value == "None":
         default_value = " = None"
@@ -159,7 +178,7 @@ def build_doc(name: str, desc: Dict[str, Any]):
         doc = doc.replace("[element_type]", name)
     # This won't work for Scenario Management and Block elements
     doc = re.sub(r"(href=\")\.\.((?:.*?)\")", r"\1" + taipy_doc_url + name + r"/../..\2", doc)
-    doc = re.sub(r"<tt>([\w_]+)</tt>", r"`\1`", doc) # <tt> not processed properly by markdownify()
+    doc = re.sub(r"<tt>([\w_]+)</tt>", r"`\1`", doc)  # <tt> not processed properly by markdownify()
     doc = "\n  ".join(markdownify(doc).split("\n"))
     # <, >, `, [, -, _ and * prefixed with a \
     doc = doc.replace("  \n", "  \\n").replace("\\<", "<").replace("\\>", ">").replace("\\`", "`")
@@ -172,56 +191,74 @@ def build_doc(name: str, desc: Dict[str, Any]):
     return f"{desc['name']}{desc['dynamic']}{desc['indexed']}\\n  {doc}\\n\\n"
 
 
-element_template = """
+def element_template(name: str, base_class: str, n: str, properties_decl: str, properties_doc: str, ind: str):
+    return f"""
 
-class {{name}}(_{{base_class}}):
-    _ELEMENT_NAME: str
-    def __init__(self, {{properties_decl}}) -> None:
-        \"\"\"Creates a{{n}} {{name}} element.\\n\\nParameters\\n----------\\n\\n{{properties_doc}}\"\"\"  # noqa: E501
-        ...
+{ind}class {name}(_{base_class}):
+{ind}    _ELEMENT_NAME: str
+{ind}    def __init__(self, {properties_decl}) -> None:
+{ind}        \"\"\"Creates a{n} {name} element.\\n\\nParameters\\n----------\\n\\n{properties_doc}\"\"\"  # noqa: E501
+{ind}        ...
 """
 
 
-def generate_elements(category: str, base_class: str):
-    for element in viselements[category]:
-        name = element[0]
-        desc = element[1]
-        properties_doc = ""
-        property_list: List[Dict[str, Any]] = []
-        property_names: List[str] = []
-        properties = resolve_inherit(name, desc["properties"], desc.get("inherits", None), viselements)
-        # Remove hidden properties
-        properties = [p for p in properties if not p.get("hide", False)]
-        # Generate function parameters
-        properties_decl = [format_as_parameter(p) for p in properties]
-        # Generate properties doc
-        for property in properties:
-            if "default_property" in property and property["default_property"] is True:
-                property_list.insert(0, property)
-                property_names.insert(0, property["name"])
-                continue
-            property_list.append(property)
-            property_names.append(property["name"])
-        # Append properties doc to element doc (once ordered)
-        for property in property_list:
-            property_doc = build_doc(name, property)
-            properties_doc += property_doc
-        if len(properties_decl) > 1:
-            properties_decl.insert(1, "*")
-        # Append element to __init__.pyi
-        with open(builder_pyi_file, "a") as file:
-            n = "n" if name[0] in ["a", "e", "i", "o"] else ""
-            file.write(
-                element_template.replace("{{name}}", name)
-                .replace("{{n}}", n)
-                .replace("{{base_class}}", base_class)
-                .replace("{{properties_decl}}", ", ".join(properties_decl))
-                .replace("{{properties_doc}}", properties_doc)
+def generate_elements(elements_by_prefix: Dict[str, List], base_class: str):
+    for prefix, elements in elements_by_prefix.items():
+        if not elements:
+            continue
+        indent = ""
+        if prefix:
+            indent = "    "
+            with open(builder_pyi_file, "a") as file:
+                file.write(prefix + "\n")
+        for element in elements:
+            name = element[0]
+            desc = element[1]
+            properties_doc = ""
+            property_list: List[Dict[str, Any]] = []
+            property_names: List[str] = []
+            properties = resolve_inherit(
+                name,
+                desc["properties"],
+                desc.get("inherits", None),
+                blocks.get(prefix, []),
+                controls.get(prefix, []),
+                undocumented.get(prefix, []),
             )
+            # Remove hidden properties
+            properties = [p for p in properties if not p.get("hide", False)]
+            # Generate function parameters
+            properties_decl = [format_as_parameter(p) for p in properties]
+            # Generate properties doc
+            for property in properties:
+                if "default_property" in property and property["default_property"] is True:
+                    property_list.insert(0, property)
+                    property_names.insert(0, property["name"])
+                    continue
+                property_list.append(property)
+                property_names.append(property["name"])
+            # Append properties doc to element doc (once ordered)
+            for property in property_list:
+                property_doc = build_doc(name, property)
+                properties_doc += property_doc
+            if len(properties_decl) > 1:
+                properties_decl.insert(1, "*")
+            # Append element to __init__.pyi
+            with open(builder_pyi_file, "a") as file:
+                file.write(
+                    element_template(
+                        name,
+                        base_class,
+                        "n" if name[0] in ["a", "e", "i", "o"] else "",
+                        ", ".join(properties_decl),
+                        properties_doc,
+                        indent,
+                    )
+                )
 
 
-generate_elements("controls", "Control")
-generate_elements("blocks", "Block")
+generate_elements(controls, "Control")
+generate_elements(blocks, "Block")
 
 os.system(f"pipenv run isort {gui_pyi_file}")
 os.system(f"pipenv run black {gui_pyi_file}")

+ 1 - 1
tools/packages/pipfiles/Pipfile3.10.max

@@ -42,7 +42,7 @@ types-tzlocal = "*"
 python_version = "3"
 
 [pipenv]
-allow_prereleases = true
+allow_prereleases = false
 
 [dev-packages.moto]
 extras = [ "s3",]

+ 1 - 1
tools/packages/pipfiles/Pipfile3.11.max

@@ -42,7 +42,7 @@ types-tzlocal = "*"
 python_version = "3"
 
 [pipenv]
-allow_prereleases = true
+allow_prereleases = false
 
 [dev-packages.moto]
 extras = [ "s3",]

+ 1 - 1
tools/packages/pipfiles/Pipfile3.12.max

@@ -42,7 +42,7 @@ types-tzlocal = "*"
 python_version = "3"
 
 [pipenv]
-allow_prereleases = true
+allow_prereleases = false
 
 [dev-packages.moto]
 extras = [ "s3",]

+ 1 - 1
tools/packages/pipfiles/Pipfile3.9.max

@@ -42,7 +42,7 @@ types-tzlocal = "*"
 python_version = "3"
 
 [pipenv]
-allow_prereleases = true
+allow_prereleases = false
 
 [dev-packages.moto]
 extras = [ "s3",]

+ 97 - 0
tools/release/bump_version.py

@@ -0,0 +1,97 @@
+# 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 json
+import os
+import re
+from dataclasses import asdict, dataclass
+from typing import Optional
+
+
+@dataclass
+class Version:
+    major: str
+    minor: str
+    patch: str
+    ext: Optional[str] = None
+
+    def bump_ext_version(self) -> None:
+        if not self.ext:
+            return
+        reg = re.compile(r"[0-9]+$")
+        num = reg.findall(self.ext)[0]
+
+        self.ext = self.ext.replace(num, str(int(num) + 1))
+
+    def validate_suffix(self, suffix="dev"):
+        if suffix not in self.ext:
+            raise Exception(f"Version does not contain suffix {suffix}")
+
+    @property
+    def name(self) -> str:
+        """returns a string representation of a version"""
+        return f"{self.major}.{self.minor}.{self.patch}"
+
+    @property
+    def dev_name(self) -> str:
+        """returns a string representation of a version"""
+        return f"{self.name}.{self.ext}"
+
+    def __str__(self) -> str:
+        """returns a string representation of a version"""
+        version_str = f"{self.major}.{self.minor}.{self.patch}"
+        if self.ext:
+            version_str = f"{version_str}.{self.ext}"
+        return version_str
+
+
+def __load_version_from_path(base_path: str) -> Version:
+    """Load version.json file from base path."""
+    with open(os.path.join(base_path, "version.json")) as version_file:
+        data = json.load(version_file)
+        return Version(**data)
+
+
+def __write_version_to_path(base_path: str, version: Version) -> None:
+    with open(os.path.join(base_path, "version.json"), "w") as version_file:
+        json.dump(asdict(version), version_file)
+
+
+def extract_version(base_path: str) -> Version:
+    """
+    Load version.json file from base path and return the version string.
+    """
+    return __load_version_from_path(base_path)
+
+
+def bump_ext_version(version: Version, _base_path: str) -> None:
+    version.bump_ext_version()
+    __write_version_to_path(_base_path, version)
+
+
+
+if __name__ == "__main__":
+    paths = (
+         [
+            f"taipy{os.sep}common",
+            f"taipy{os.sep}core",
+            f"taipy{os.sep}rest",
+            f"taipy{os.sep}gui",
+            f"taipy{os.sep}templates",
+            "taipy",
+        ]
+    )
+
+    for _path in paths:
+        _version = extract_version(_path)
+        bump_ext_version(_version, _path)
+    print(f"NEW_VERSION={_version.dev_name}") # noqa T201 # type: ignore[reportPossiblyUnboundVariable]
+

+ 3 - 2
tools/release/setup_version.py

@@ -77,12 +77,13 @@ def __setup_dev_version(version: Version, _base_path: str, name: Optional[str] =
     version.validate_suffix()
 
     name = f"{name}_VERSION" if name else "VERSION"
+
     print(f"{name}={version.dev_name}")  # noqa: T201
 
-    version.bump_ext_version()
 
+def bump_ext_version(version: Version, _base_path: str) -> None:
+    version.bump_ext_version()
     __write_version_to_path(_base_path, version)
-    print(f"NEW_{name}={version.dev_name}")  # noqa: T201
 
 
 def __setup_prod_version(version: Version, target_version: str, branch_name: str, name: str = None) -> None: