1
0
namnguyen 1 жил өмнө
parent
commit
83c0692536

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

@@ -11,11 +11,11 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
 module.exports = {
 module.exports = {
  // testEnvironment: 'jest-environment-jsdom',
  // testEnvironment: 'jest-environment-jsdom',
   preset: 'ts-jest',
   preset: 'ts-jest',
   testEnvironment: 'jsdom',
   testEnvironment: 'jsdom',
-  setupFiles: ['./test-config/jest.env.js', './test-config/createObjectUrl.js', './test-config/Canvas.js', './test-config/mockFileUpload.js'],
+  setupFiles: ['./test-config/jest.env.js', './test-config/createObjectUrl.js', './test-config/Canvas.js', './test-config/mockFileUpload.js', './test-config/intersectionObserver.js'],
   coverageReporters: ["json", "html", "text"],
   coverageReporters: ["json", "html", "text"],
 };
 };

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 216 - 311
frontend/taipy-gui/package-lock.json


+ 3 - 3
frontend/taipy-gui/package.json

@@ -74,7 +74,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@testing-library/jest-dom": "^6.1.3",
     "@testing-library/jest-dom": "^6.1.3",
-    "@testing-library/react": "^14.0.0",
+    "@testing-library/react": "^15.0.7",
     "@testing-library/user-event": "^14.2.1",
     "@testing-library/user-event": "^14.2.1",
     "@types/css-mediaquery": "^0.1.1",
     "@types/css-mediaquery": "^0.1.1",
     "@types/jest": "^29.0.1",
     "@types/jest": "^29.0.1",
@@ -94,11 +94,11 @@
     "autoprefixer": "^10.4.0",
     "autoprefixer": "^10.4.0",
     "copy-webpack-plugin": "^12.0.1",
     "copy-webpack-plugin": "^12.0.1",
     "cross-env": "^7.0.3",
     "cross-env": "^7.0.3",
-    "css-loader": "^6.5.0",
+    "css-loader": "^7.1.0",
     "css-mediaquery": "^0.1.2",
     "css-mediaquery": "^0.1.2",
     "dotenv-webpack": "^8.0.0",
     "dotenv-webpack": "^8.0.0",
     "dts-bundle-generator": "^9.2.1",
     "dts-bundle-generator": "^9.2.1",
-    "eslint": "^8.3.0",
+    "eslint": "^8.57.0",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-tsdoc": "^0.2.16",
     "eslint-plugin-tsdoc": "^0.2.16",

+ 121 - 0
frontend/taipy-gui/src/components/Taipy/Chat.spec.tsx

@@ -0,0 +1,121 @@
+/*
+ * 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 } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import userEvent from "@testing-library/user-event";
+
+import Chat from "./Chat";
+import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers";
+import { TaipyContext } from "../../context/taipyContext";
+import { stringIcon } from "../../utils/icon";
+import { TableValueType } from "./tableUtils";
+
+const valueKey = "Infinite-Entity--asc";
+const messages: TableValueType = {
+    [valueKey]: {
+        data: [
+    ["1", "msg 1", "Fred"],
+    ["2", "msg From Another unknown User", "Fredo"],
+    ["3", "This from the sender User", "taipy"],
+    ["4", "And from another known one", "Fredi"],
+], rowcount: 4, start: 0}};
+const user1: [string, stringIcon] = ["Fred", { path: "/images/favicon.png", text: "Fred.png" }];
+const user2: [string, stringIcon] = ["Fredi", { path: "/images/fred.png", text: "Fredi.png" }];
+const users = [user1, user2];
+
+const searchMsg = messages[valueKey].data[0][1];
+
+describe("Chat Component", () => {
+    it("renders", async () => {
+        const { getByText, getByLabelText } = render(<Chat messages={messages} defaultKey={valueKey} />);
+        const elt = getByText(searchMsg);
+        expect(elt.tagName).toBe("DIV");
+        const input = getByLabelText("message (taipy)");
+        expect(input.tagName).toBe("INPUT");
+    });
+    it("uses the class", async () => {
+        const { getByText } = render(<Chat messages={messages} className="taipy-chat" defaultKey={valueKey} />);
+        const elt = getByText(searchMsg);
+        expect(elt.parentElement?.parentElement?.parentElement?.parentElement).toHaveClass("taipy-chat");
+    });
+    it("can display an avatar", async () => {
+        const { getByAltText } = render(<Chat messages={messages} users={users} defaultKey={valueKey} />);
+        const elt = getByAltText("Fred.png");
+        expect(elt.tagName).toBe("IMG");
+    });
+    it("is disabled", async () => {
+        const { getAllByRole } = render(<Chat messages={messages} active={false} defaultKey={valueKey} />);
+        const elts = getAllByRole("button");
+        elts.forEach((elt) => expect(elt).toHaveClass("Mui-disabled"));
+    });
+    it("is enabled by default", async () => {
+        const { getAllByRole } = render(<Chat messages={messages} defaultKey={valueKey} />);
+        const elts = getAllByRole("button");
+        elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
+    });
+    it("is enabled by active", async () => {
+        const { getAllByRole } = render(<Chat messages={messages} active={true} defaultKey={valueKey} />);
+        const elts = getAllByRole("button");
+        elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
+    });
+    it("can hide input", async () => {
+        render(<Chat messages={messages} withInput={false} className="taipy-chat" defaultKey={valueKey} />);
+        const elt = document.querySelector(".taipy-chat input");
+        expect(elt).toBeNull();
+    });
+    it("dispatch a well formed message by Keyboard", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const { getByLabelText } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Chat messages={messages} updateVarName="varname" defaultKey={valueKey} />
+            </TaipyContext.Provider>
+        );
+        const elt = getByLabelText("message (taipy)");
+        await userEvent.click(elt);
+        await userEvent.keyboard("new message{Enter}");
+        expect(dispatch).toHaveBeenCalledWith({
+            type: "SEND_ACTION_ACTION",
+            name: "",
+            context: undefined,
+            payload: {
+                action: undefined,
+                args: ["Enter", "varname", "new message", "taipy"],
+            },
+        });
+    });
+    it("dispatch a well formed message by button", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const { getByLabelText, getByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Chat messages={messages} updateVarName="varname" defaultKey={valueKey} />
+            </TaipyContext.Provider>
+        );
+        const elt = getByLabelText("message (taipy)");
+        await userEvent.click(elt);
+        await userEvent.keyboard("new message");
+        await userEvent.click(getByRole("button"))
+        expect(dispatch).toHaveBeenCalledWith({
+            type: "SEND_ACTION_ACTION",
+            name: "",
+            context: undefined,
+            payload: {
+                action: undefined,
+                args: ["click", "varname", "new message", "taipy"],
+            },
+        });
+    });
+});

+ 413 - 0
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -0,0 +1,413 @@
+/*
+ * 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, { useMemo, useCallback, KeyboardEvent, MouseEvent, useState, useRef, useEffect, ReactNode } from "react";
+import { SxProps, Theme, darken, lighten } from "@mui/material/styles";
+import Avatar from "@mui/material/Avatar";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Chip from "@mui/material/Chip";
+import Grid from "@mui/material/Grid";
+import IconButton from "@mui/material/IconButton";
+import InputAdornment from "@mui/material/InputAdornment";
+import Paper from "@mui/material/Paper";
+import Popper from "@mui/material/Popper";
+import TextField from "@mui/material/TextField";
+import Tooltip from "@mui/material/Tooltip";
+import Send from "@mui/icons-material/Send";
+import ArrowDownward from "@mui/icons-material/ArrowDownward";
+import ArrowUpward from "@mui/icons-material/ArrowUpward";
+
+// import InfiniteLoader from "react-window-infinite-loader";
+
+import { createRequestInfiniteTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
+import { TaipyActiveProps, disableColor, getSuffixedClassNames } from "./utils";
+import { useClassNames, useDispatch, useDynamicProperty, useElementVisible, useModule } from "../../utils/hooks";
+import { LoVElt, useLovListMemo } from "./lovUtils";
+import { IconAvatar, avatarSx } from "../../utils/icon";
+import { getInitials } from "../../utils";
+import { RowType, TableValueType } from "./tableUtils";
+
+interface ChatProps extends TaipyActiveProps {
+    messages?: TableValueType;
+    withInput?: boolean;
+    users?: LoVElt[];
+    defaultUsers?: string;
+    onAction?: string;
+    senderId?: string;
+    height?: string;
+    defaultKey?: string; // for testing purposes only
+    pageSize?: number;
+}
+
+const ENTER_KEY = "Enter";
+
+const indicWidth = 0.7;
+const avatarWidth = 24;
+const chatAvatarSx = { ...avatarSx, width: avatarWidth, height: avatarWidth };
+const avatarColSx = { width: 1.5 * avatarWidth };
+const senderMsgSx = { width: "fit-content", maxWidth: "80%", marginLeft: "auto" };
+const gridSx = { pb: "1em", mt: "unset", flex: 1, overflow: "auto" };
+const loadMoreSx = { width: "fit-content", marginLeft: "auto", marginRight: "auto" };
+const inputSx = { maxWidth: "unset" };
+const nameSx = { fontSize: "0.6em", fontWeight: "bolder" };
+const senderPaperSx = {
+    pr: `${indicWidth}em`,
+    pl: `${indicWidth}em`,
+    mr: `${indicWidth}em`,
+    position: "relative",
+    "&:before": {
+        content: "''",
+        position: "absolute",
+        width: "0",
+        height: "0",
+        borderTopWidth: `${indicWidth}em`,
+        borderTopStyle: "solid",
+        borderTopColor: (theme: Theme) => theme.palette.background.paper,
+        borderLeft: `${indicWidth}em solid transparent`,
+        borderRight: `${indicWidth}em solid transparent`,
+        top: "0",
+        right: `-${indicWidth}em`,
+    },
+} as SxProps<Theme>;
+const otherPaperSx = {
+    position: "relative",
+    pl: `${indicWidth}em`,
+    pr: `${indicWidth}em`,
+    "&:before": {
+        content: "''",
+        position: "absolute",
+        width: "0",
+        height: "0",
+        borderTopWidth: `${indicWidth}em`,
+        borderTopStyle: "solid",
+        borderTopColor: (theme: Theme) => theme.palette.background.paper,
+        borderLeft: `${indicWidth}em solid transparent`,
+        borderRight: `${indicWidth}em solid transparent`,
+        top: "0",
+        left: `-${indicWidth}em`,
+    },
+} as SxProps<Theme>;
+const defaultBoxSx = {
+    pl: `${indicWidth}em`,
+    pr: `${indicWidth}em`,
+    backgroundColor: (theme: Theme) =>
+        theme.palette.mode == "dark"
+            ? lighten(theme.palette.background.paper, 0.05)
+            : darken(theme.palette.background.paper, 0.15),
+} as SxProps<Theme>;
+const noAnchorSx = { overflowAnchor: "none", "& *": { overflowAnchor: "none" } } as SxProps<Theme>;
+const anchorSx = { overflowAnchor: "auto", height: "1px", width: "100%" } as SxProps<Theme>;
+
+interface key2Rows {
+    key: string;
+}
+
+interface ChatRowProps {
+    senderId: string;
+    message: string;
+    name: string;
+    className?: string;
+    getAvatar: (id: string) => ReactNode;
+    index: number;
+}
+
+const ChatRow = (props: ChatRowProps) => {
+    const { senderId, message, name, className, getAvatar, index } = props;
+    return senderId == name ? (
+        <Grid item className={getSuffixedClassNames(className, "-sent")} xs={12} sx={noAnchorSx}>
+            <Box sx={senderMsgSx}>
+                <Paper sx={senderPaperSx} data-idx={index}>
+                    {message}
+                </Paper>
+            </Box>
+        </Grid>
+    ) : (
+        <Grid
+            item
+            container
+            className={getSuffixedClassNames(className, "-received")}
+            rowSpacing={0.2}
+            columnSpacing={1}
+            sx={noAnchorSx}
+        >
+            <Grid item sx={avatarColSx}></Grid>
+            <Grid item sx={nameSx}>
+                {name}
+            </Grid>
+            <Box width="100%" />
+            <Grid item sx={avatarColSx}>
+                {getAvatar(name)}
+            </Grid>
+            <Grid item>
+                <Paper sx={otherPaperSx} data-idx={index}>
+                    {message}
+                </Paper>
+            </Grid>
+        </Grid>
+    );
+};
+
+const getChatKey = (start: number, page: number) => `Chat-${start}-${start+page}`
+
+const Chat = (props: ChatProps) => {
+    const { id, updateVarName, senderId = "taipy", onAction, withInput = true, defaultKey = "", pageSize = 50 } = props;
+    const dispatch = useDispatch();
+    const module = useModule();
+
+    const [rows, setRows] = useState<RowType[]>([]);
+    const page = useRef<key2Rows>({ key: defaultKey });
+    const [rowCount, setRowCount] = useState(0);
+    const [columns, setColumns] = useState<Array<string>>([]);
+    const scrollDivRef = useRef<HTMLDivElement>(null);
+    const anchorDivRef = useRef<HTMLElement>(null);
+    const isAnchorDivVisible = useElementVisible(anchorDivRef);
+    const [showMessage, setShowMessage] = useState(false);
+    const [anchorPopup, setAnchorPopup] = useState<HTMLDivElement | null>(null);
+
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+    const active = useDynamicProperty(props.active, props.defaultActive, true);
+    const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
+    const users = useLovListMemo(props.users, props.defaultUsers || "");
+
+    const boxSx = useMemo(
+        () =>
+            props.height
+                ? ({
+                      ...defaultBoxSx,
+                      maxHeight: props.height,
+                      display: "flex",
+                      flexDirection: "column",
+                  } as SxProps<Theme>)
+                : defaultBoxSx,
+        [props.height]
+    );
+    const handleAction = useCallback(
+        (evt: KeyboardEvent<HTMLDivElement>) => {
+            if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && ENTER_KEY == evt.key) {
+                const elt = evt.currentTarget.querySelector("input");
+                if (elt?.value) {
+                    dispatch(
+                        createSendActionNameAction(id, module, onAction, evt.key, updateVarName, elt?.value, senderId)
+                    );
+                    elt.value = "";
+                }
+                evt.preventDefault();
+            }
+        },
+        [updateVarName, onAction, senderId, id, dispatch, module]
+    );
+
+    const handleClick = useCallback(
+        (evt: MouseEvent<HTMLButtonElement>) => {
+            const elt = evt.currentTarget.parentElement?.parentElement?.querySelector("input");
+            if (elt?.value) {
+                dispatch(
+                    createSendActionNameAction(id, module, onAction, "click", updateVarName, elt?.value, senderId)
+                );
+                elt.value = "";
+            }
+            evt.preventDefault();
+        },
+        [updateVarName, onAction, senderId, id, dispatch, module]
+    );
+
+    const avatars = useMemo(() => {
+        return users.reduce((pv, elt) => {
+            if (elt.id) {
+                pv[elt.id] =
+                    typeof elt.item == "string" ? (
+                        <Tooltip title={elt.item}>
+                            <Avatar sx={chatAvatarSx}>{getInitials(elt.item)}</Avatar>
+                        </Tooltip>
+                    ) : (
+                        <IconAvatar img={elt.item} sx={chatAvatarSx} />
+                    );
+            }
+            return pv;
+        }, {} as Record<string, React.ReactNode>);
+    }, [users]);
+
+    const getAvatar = useCallback(
+        (id: string) =>
+            avatars[id] || (
+                <Tooltip title={id}>
+                    <Avatar sx={chatAvatarSx}>{getInitials(id)}</Avatar>
+                </Tooltip>
+            ),
+        [avatars]
+    );
+
+    const loadMoreItems = useCallback(
+        (startIndex: number) => {
+            const key = getChatKey(startIndex, pageSize);
+            page.current = {
+                key: key,
+            };
+            dispatch(
+                createRequestInfiniteTableUpdateAction(
+                    updateVarName,
+                    id,
+                    module,
+                    [],
+                    key,
+                    startIndex,
+                    startIndex + pageSize,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    undefined,
+                    true // reverse
+                )
+            );
+        },
+        [pageSize, updateVarName, id, dispatch, module]
+    );
+
+    const showBottom = useCallback(() => {
+        anchorDivRef.current?.scrollIntoView();
+        setShowMessage(false);
+    }, []);
+
+    const refresh = typeof props.messages === "number";
+
+    useEffect(() => {
+        if (!refresh && props.messages && page.current.key && props.messages[page.current.key] !== undefined) {
+            const newValue = props.messages[page.current.key];
+            setRowCount(newValue.rowcount);
+            const nr = newValue.data as RowType[];
+            if (Array.isArray(nr) && nr.length > newValue.start && nr[newValue.start]) {
+                setRows((old) => {
+                    old.length && nr.length > old.length && setShowMessage(true);
+                    if (nr.length < old.length) {
+                        return nr.concat(old.slice(nr.length))
+                    }
+                    if (old.length > newValue.start) {
+                        return old.slice(0, newValue.start).concat(nr.slice(newValue.start));
+                    }
+                    return nr;
+                });
+                const cols = Object.keys(nr[newValue.start]);
+                setColumns(cols.length > 2 ? cols : cols.length == 2 ? [...cols, ""] : ["", ...cols, "", ""]);
+            }
+            page.current.key = getChatKey(0, pageSize);
+        }
+    }, [refresh, pageSize, props.messages]);
+
+    useEffect(() => {
+        if (showMessage && !isAnchorDivVisible) {
+            setAnchorPopup(scrollDivRef.current);
+            setTimeout(() => setShowMessage(false), 5000);
+        } else if (!showMessage) {
+            setAnchorPopup(null);
+        }
+    }, [showMessage, isAnchorDivVisible]);
+
+    useEffect(() => {
+        if (refresh) {
+            setTimeout(() => loadMoreItems(0), 1); // So that the state can be changed
+        }
+    }, [refresh, loadMoreItems]);
+
+    useEffect(() => {
+        loadMoreItems(0);
+    }, [loadMoreItems]);
+
+    const loadOlder = useCallback(
+        (evt: MouseEvent<HTMLElement>) => {
+            const { start } = evt.currentTarget.dataset;
+            if (start) {
+                loadMoreItems(parseInt(start));
+            }
+        },
+        [loadMoreItems]
+    );
+
+    return (
+        <Tooltip title={hover || "" || `rowCount: ${rowCount}`}>
+            <Paper className={className} sx={boxSx} id={id}>
+                <Grid container rowSpacing={2} sx={gridSx} ref={scrollDivRef}>
+                    {rows.length && !rows[0] ? (
+                        <Grid item className={getSuffixedClassNames(className, "-load")} xs={12} sx={noAnchorSx}>
+                            <Box sx={loadMoreSx}>
+                                <Button
+                                    endIcon={<ArrowUpward />}
+                                    onClick={loadOlder}
+                                    data-start={rows.length - rows.findIndex((row) => !!row)}
+                                >
+                                    Load More
+                                </Button>
+                            </Box>
+                        </Grid>
+                    ) : null}
+                    {rows.map((row, idx) =>
+                        row ? (
+                            <ChatRow
+                                key={columns[0] ? `${row[columns[0]]}` : `id${idx}`}
+                                senderId={senderId}
+                                message={`${row[columns[1]]}`}
+                                name={columns[2] ? `${row[columns[2]]}` : "Unknown"}
+                                className={className}
+                                getAvatar={getAvatar}
+                                index={idx}
+                            />
+                        ) : null
+                    )}
+                    <Box sx={anchorSx} ref={anchorDivRef} />
+                </Grid>
+                <Popper id={id} open={Boolean(anchorPopup)} anchorEl={anchorPopup} placement="right">
+                    <Chip
+                        label="A new message is available"
+                        variant="outlined"
+                        onClick={showBottom}
+                        icon={<ArrowDownward />}
+                    />
+                </Popper>
+                {withInput ? (
+                    <TextField
+                        margin="dense"
+                        fullWidth
+                        className={getSuffixedClassNames(className, "-input")}
+                        label={`message (${senderId})`}
+                        disabled={!active}
+                        onKeyDown={handleAction}
+                        InputProps={{
+                            endAdornment: (
+                                <InputAdornment position="end">
+                                    <IconButton
+                                        aria-label="send message"
+                                        onClick={handleClick}
+                                        edge="end"
+                                        disabled={!active}
+                                    >
+                                        <Send color={disableColor("primary", !active)} />
+                                    </IconButton>
+                                </InputAdornment>
+                            ),
+                        }}
+                        sx={inputSx}
+                    />
+                ) : null}
+            </Paper>
+        </Tooltip>
+    );
+};
+
+export default Chat;

+ 2 - 0
frontend/taipy-gui/src/components/Taipy/index.ts

@@ -13,6 +13,7 @@
 
 
 import { ComponentType } from "react";
 import { ComponentType } from "react";
 import Button from "./Button";
 import Button from "./Button";
+import Chat from "./Chat";
 import Chart from "./Chart";
 import Chart from "./Chart";
 import DateRange from "./DateRange";
 import DateRange from "./DateRange";
 import DateSelector from "./DateSelector";
 import DateSelector from "./DateSelector";
@@ -47,6 +48,7 @@ export const getRegisteredComponents = () => {
         Object.entries({
         Object.entries({
             a: Link,
             a: Link,
             Button: Button,
             Button: Button,
+            Chat: Chat,
             Chart: Chart,
             Chart: Chart,
             DateRange: DateRange,
             DateRange: DateRange,
             DateSelector: DateSelector,
             DateSelector: DateSelector,

+ 2 - 0
frontend/taipy-gui/src/components/Taipy/utils.ts

@@ -111,3 +111,5 @@ export const getSuffixedClassNames = (names: string | undefined, suffix: string)
         .join(" ");
         .join(" ");
 
 
 export const emptyStyle = {} as CSSProperties;
 export const emptyStyle = {} as CSSProperties;
+
+export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);

+ 50 - 34
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -582,8 +582,8 @@ const ligtenPayload = (payload: Record<string, unknown>) => {
             pv[key] = payload[key];
             pv[key] = payload[key];
         }
         }
         return pv;
         return pv;
-    }, {} as typeof payload)
-}
+    }, {} as typeof payload);
+};
 
 
 export const createRequestTableUpdateAction = (
 export const createRequestTableUpdateAction = (
     name: string | undefined,
     name: string | undefined,
@@ -605,21 +605,28 @@ export const createRequestTableUpdateAction = (
     compareDatas?: string,
     compareDatas?: string,
     stateContext?: Record<string, unknown>
     stateContext?: Record<string, unknown>
 ): TaipyAction =>
 ): TaipyAction =>
-    createRequestDataUpdateAction(name, id, context, columns, pageKey, ligtenPayload({
-        start: start,
-        end: end,
-        orderby: orderBy,
-        sort: sort,
-        aggregates: aggregates,
-        applies: applies,
-        styles: styles,
-        tooltips: tooltips,
-        handlenan: handleNan,
-        filters: filters,
-        compare: compare,
-        compare_datas: compareDatas,
-        state_context: stateContext,
-    }));
+    createRequestDataUpdateAction(
+        name,
+        id,
+        context,
+        columns,
+        pageKey,
+        ligtenPayload({
+            start: start,
+            end: end,
+            orderby: orderBy,
+            sort: sort,
+            aggregates: aggregates,
+            applies: applies,
+            styles: styles,
+            tooltips: tooltips,
+            handlenan: handleNan,
+            filters: filters,
+            compare: compare,
+            compare_datas: compareDatas,
+            state_context: stateContext,
+        })
+    );
 
 
 export const createRequestInfiniteTableUpdateAction = (
 export const createRequestInfiniteTableUpdateAction = (
     name: string | undefined,
     name: string | undefined,
@@ -639,24 +646,33 @@ export const createRequestInfiniteTableUpdateAction = (
     filters?: Array<FilterDesc>,
     filters?: Array<FilterDesc>,
     compare?: string,
     compare?: string,
     compareDatas?: string,
     compareDatas?: string,
-    stateContext?: Record<string, unknown>
+    stateContext?: Record<string, unknown>,
+    reverse?: boolean
 ): TaipyAction =>
 ): TaipyAction =>
-    createRequestDataUpdateAction(name, id, context, columns, pageKey, ligtenPayload({
-        infinite: true,
-        start: start,
-        end: end,
-        orderby: orderBy,
-        sort: sort,
-        aggregates: aggregates,
-        applies: applies,
-        styles: styles,
-        tooltips: tooltips,
-        handlenan: handleNan,
-        filters: filters,
-        compare: compare,
-        compare_datas: compareDatas,
-        state_context: stateContext,
-    }));
+    createRequestDataUpdateAction(
+        name,
+        id,
+        context,
+        columns,
+        pageKey,
+        ligtenPayload({
+            infinite: true,
+            start: start,
+            end: end,
+            orderby: orderBy,
+            sort: sort,
+            aggregates: aggregates,
+            applies: applies,
+            styles: styles,
+            tooltips: tooltips,
+            handlenan: handleNan,
+            filters: filters,
+            compare: compare,
+            compare_datas: compareDatas,
+            state_context: stateContext,
+            reverse: !!reverse,
+        })
+    );
 
 
 /**
 /**
  * Create a *request data update* `Action` that will be used to update the `Context`.
  * Create a *request data update* `Action` that will be used to update the `Context`.

+ 2 - 0
frontend/taipy-gui/src/extensions/exports.ts

@@ -28,6 +28,7 @@ import {
     useClassNames,
     useClassNames,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     useDispatch,
     useDispatch,
+    useDynamicJsonProperty,
     useDynamicProperty,
     useDynamicProperty,
     useModule,
     useModule,
 } from "../utils/hooks";
 } from "../utils/hooks";
@@ -55,6 +56,7 @@ export {
     useClassNames,
     useClassNames,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     useDispatch,
     useDispatch,
+    useDynamicJsonProperty,
     useDynamicProperty,
     useDynamicProperty,
     useLovListMemo,
     useLovListMemo,
     useModule,
     useModule,

+ 25 - 4
frontend/taipy-gui/src/utils/hooks.ts

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import { Dispatch, useContext, useEffect, useMemo, useRef } from "react";
+import { Dispatch, RefObject, useContext, useEffect, useMemo, useRef, useState } from "react";
 import { useMediaQuery, useTheme } from "@mui/material";
 import { useMediaQuery, useTheme } from "@mui/material";
 
 
 import { getUpdateVars } from "../components/Taipy/utils";
 import { getUpdateVars } from "../components/Taipy/utils";
@@ -92,7 +92,7 @@ export const useDispatchRequestUpdateOnFirstRender = (
     forceRefresh?: boolean
     forceRefresh?: boolean
 ) => {
 ) => {
     useEffect(() => {
     useEffect(() => {
-        const updateArray = getUpdateVars(updateVars).filter(uv => !uv.includes(","));
+        const updateArray = getUpdateVars(updateVars).filter((uv) => !uv.includes(","));
         varName && updateArray.push(varName);
         varName && updateArray.push(varName);
         updateArray.length && dispatch(createRequestUpdateAction(id, context, updateArray, forceRefresh));
         updateArray.length && dispatch(createRequestUpdateAction(id, context, updateArray, forceRefresh));
     }, [updateVars, dispatch, id, context, varName, forceRefresh]);
     }, [updateVars, dispatch, id, context, varName, forceRefresh]);
@@ -157,7 +157,7 @@ export const useClassNames = (libClassName?: string, dynamicClassName?: string,
 export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>): void => {
 export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>): void => {
     // Get a mutable ref object where we can store props ...
     // Get a mutable ref object where we can store props ...
     // ... for comparison next time this hook runs.
     // ... for comparison next time this hook runs.
-    const previousProps = useRef({} as Record<string, unknown>);
+    const previousProps = useRef<Record<string, unknown>>();
     useEffect(() => {
     useEffect(() => {
         if (previousProps.current) {
         if (previousProps.current) {
             // Get all keys from previous and current props
             // Get all keys from previous and current props
@@ -167,7 +167,7 @@ export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>)
             // Iterate through keys
             // Iterate through keys
             allKeys.forEach((key) => {
             allKeys.forEach((key) => {
                 // If previous is different from current
                 // If previous is different from current
-                if (previousProps.current[key] !== props[key]) {
+                if (previousProps.current && previousProps.current[key] !== props[key]) {
                     // Add to changesObj
                     // Add to changesObj
                     changesObj[key] = {
                     changesObj[key] = {
                         from: previousProps.current[key],
                         from: previousProps.current[key],
@@ -184,3 +184,24 @@ export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>)
         previousProps.current = props;
         previousProps.current = props;
     });
     });
 };
 };
+
+export const useElementVisible = (ref: RefObject<HTMLElement>) => {
+    const observerRef = useRef<IntersectionObserver | null>(null);
+    const [isOnScreen, setIsOnScreen] = useState(false);
+
+    useEffect(() => {
+        observerRef.current = new IntersectionObserver(([entry]) => setIsOnScreen(entry.isIntersecting));
+    }, []);
+
+    useEffect(() => {
+        observerRef.current && ref.current && observerRef.current.observe(ref.current);
+
+        return () => {
+            observerRef.current && observerRef.current.disconnect();
+        };
+    }, [ref]);
+
+    return isOnScreen;
+};
+
+export const useUniqueId = (id?: string) => useMemo(() => (id ? id : new Date().toISOString() + Math.random()), [id]);

+ 2 - 2
frontend/taipy-gui/src/utils/icon.tsx

@@ -14,7 +14,7 @@
 import React, { useEffect, useMemo, useRef } from "react";
 import React, { useEffect, useMemo, useRef } from "react";
 import axios from "axios";
 import axios from "axios";
 import Avatar from "@mui/material/Avatar";
 import Avatar from "@mui/material/Avatar";
-import { SxProps, useTheme, Theme } from "@mui/system";
+import { SxProps, useTheme, Theme } from "@mui/material/styles";
 
 
 /**
 /**
  * An Icon representation.
  * An Icon representation.
@@ -39,7 +39,7 @@ interface IconProps {
     id?: string;
     id?: string;
     img: Icon;
     img: Icon;
     className?: string;
     className?: string;
-    sx?: SxProps;
+    sx?: SxProps<Theme>;
 }
 }
 
 
 export const avatarSx = { bgcolor: (theme: Theme) => theme.palette.text.primary };
 export const avatarSx = { bgcolor: (theme: Theme) => theme.palette.text.primary };

+ 23 - 0
frontend/taipy-gui/test-config/intersectionObserver.js

@@ -0,0 +1,23 @@
+class IntersectionObserver {
+    root = null;
+    rootMargin = "";
+    thresholds = [];
+
+    disconnect() {
+      return null;
+    }
+
+    observe() {
+      return null;
+    }
+
+    takeRecords() {
+      return [];
+    }
+
+    unobserve() {
+      return null;
+    }
+  }
+  window.IntersectionObserver = IntersectionObserver;
+  global.IntersectionObserver = IntersectionObserver;

+ 171 - 202
frontend/taipy/package-lock.json

@@ -43,15 +43,6 @@
     "../../taipy/gui/webapp": {
     "../../taipy/gui/webapp": {
       "version": "3.2.0"
       "version": "3.2.0"
     },
     },
-    "node_modules/@aashutoshrathi/word-wrap": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
-      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {
       "version": "7.24.2",
       "version": "7.24.2",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
@@ -84,19 +75,19 @@
       }
       }
     },
     },
     "node_modules/@babel/helper-validator-identifier": {
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
+      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
       "engines": {
       "engines": {
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
     "node_modules/@babel/highlight": {
     "node_modules/@babel/highlight": {
-      "version": "7.24.2",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
-      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz",
+      "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==",
       "dependencies": {
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "chalk": "^2.4.2",
         "chalk": "^2.4.2",
         "js-tokens": "^4.0.0",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
         "picocolors": "^1.0.0"
@@ -170,9 +161,9 @@
       }
       }
     },
     },
     "node_modules/@babel/runtime": {
     "node_modules/@babel/runtime": {
-      "version": "7.24.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
-      "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
+      "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
       "dependencies": {
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
         "regenerator-runtime": "^0.14.0"
       },
       },
@@ -181,12 +172,12 @@
       }
       }
     },
     },
     "node_modules/@babel/types": {
     "node_modules/@babel/types": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
-      "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
+      "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
       "dependencies": {
       "dependencies": {
-        "@babel/helper-string-parser": "^7.23.4",
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-string-parser": "^7.24.1",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "to-fast-properties": "^2.0.0"
         "to-fast-properties": "^2.0.0"
       },
       },
       "engines": {
       "engines": {
@@ -414,28 +405,28 @@
       }
       }
     },
     },
     "node_modules/@floating-ui/core": {
     "node_modules/@floating-ui/core": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
-      "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz",
+      "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==",
       "dependencies": {
       "dependencies": {
-        "@floating-ui/utils": "^0.2.1"
+        "@floating-ui/utils": "^0.2.0"
       }
       }
     },
     },
     "node_modules/@floating-ui/dom": {
     "node_modules/@floating-ui/dom": {
-      "version": "1.6.3",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
-      "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz",
+      "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==",
       "dependencies": {
       "dependencies": {
         "@floating-ui/core": "^1.0.0",
         "@floating-ui/core": "^1.0.0",
         "@floating-ui/utils": "^0.2.0"
         "@floating-ui/utils": "^0.2.0"
       }
       }
     },
     },
     "node_modules/@floating-ui/react-dom": {
     "node_modules/@floating-ui/react-dom": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
-      "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz",
+      "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==",
       "dependencies": {
       "dependencies": {
-        "@floating-ui/dom": "^1.6.1"
+        "@floating-ui/dom": "^1.0.0"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
         "react": ">=16.8.0",
         "react": ">=16.8.0",
@@ -443,9 +434,9 @@
       }
       }
     },
     },
     "node_modules/@floating-ui/utils": {
     "node_modules/@floating-ui/utils": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
-      "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
+      "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
     },
     },
     "node_modules/@humanwhocodes/config-array": {
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.14",
       "version": "0.11.14",
@@ -652,18 +643,18 @@
       }
       }
     },
     },
     "node_modules/@mui/core-downloads-tracker": {
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.15.15",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz",
-      "integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==",
+      "version": "5.15.18",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.18.tgz",
+      "integrity": "sha512-/9pVk+Al8qxAjwFUADv4BRZgMpZM4m5E+2Q/20qhVPuIJWqKp4Ie4tGExac6zu93rgPTYVQGgu+1vjiT0E+cEw==",
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
         "url": "https://opencollective.com/mui-org"
       }
       }
     },
     },
     "node_modules/@mui/icons-material": {
     "node_modules/@mui/icons-material": {
-      "version": "5.15.15",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.15.tgz",
-      "integrity": "sha512-kkeU/pe+hABcYDH6Uqy8RmIsr2S/y5bP2rp+Gat4CcRjCcVne6KudS1NrZQhUCRysrTDCAhcbcf9gt+/+pGO2g==",
+      "version": "5.15.18",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.18.tgz",
+      "integrity": "sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.23.9"
         "@babel/runtime": "^7.23.9"
       },
       },
@@ -686,13 +677,13 @@
       }
       }
     },
     },
     "node_modules/@mui/material": {
     "node_modules/@mui/material": {
-      "version": "5.15.15",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz",
-      "integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==",
+      "version": "5.15.18",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.18.tgz",
+      "integrity": "sha512-n+/dsiqux74fFfcRUJjok+ieNQ7+BEk6/OwX9cLcLvriZrZb+/7Y8+Fd2HlUUbn5N0CDurgAHm0VH1DqyJ9HAw==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.23.9",
         "@babel/runtime": "^7.23.9",
         "@mui/base": "5.0.0-beta.40",
         "@mui/base": "5.0.0-beta.40",
-        "@mui/core-downloads-tracker": "^5.15.15",
+        "@mui/core-downloads-tracker": "^5.15.18",
         "@mui/system": "^5.15.15",
         "@mui/system": "^5.15.15",
         "@mui/types": "^7.2.14",
         "@mui/types": "^7.2.14",
         "@mui/utils": "^5.15.14",
         "@mui/utils": "^5.15.14",
@@ -866,16 +857,16 @@
       }
       }
     },
     },
     "node_modules/@mui/x-date-pickers": {
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.2.0.tgz",
-      "integrity": "sha512-hsXugZ+n1ZnHRYzf7+PFrjZ44T+FyGZmTreBmH0M2RUaAblgK+A1V3KNLT+r4Y9gJLH+92LwePxQ9xyfR+E51A==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.4.0.tgz",
+      "integrity": "sha512-Xh0LD/PCYIWWSchvtnEHdUfIsnANA0QOppUkCJ+4b8mN7z+TMEBA/LHmzA2+edxo7eanyfJ7L52znxwPP4vX8Q==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.0",
         "@babel/runtime": "^7.24.0",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@types/react-transition-group": "^4.4.10",
         "@types/react-transition-group": "^4.4.10",
-        "clsx": "^2.1.0",
+        "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
         "react-transition-group": "^4.4.5"
       },
       },
@@ -891,7 +882,7 @@
         "@emotion/styled": "^11.8.1",
         "@emotion/styled": "^11.8.1",
         "@mui/material": "^5.15.14",
         "@mui/material": "^5.15.14",
         "date-fns": "^2.25.0 || ^3.2.0",
         "date-fns": "^2.25.0 || ^3.2.0",
-        "date-fns-jalali": "^2.13.0-0",
+        "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
         "dayjs": "^1.10.7",
         "dayjs": "^1.10.7",
         "luxon": "^3.0.2",
         "luxon": "^3.0.2",
         "moment": "^2.29.4",
         "moment": "^2.29.4",
@@ -931,16 +922,16 @@
       }
       }
     },
     },
     "node_modules/@mui/x-tree-view": {
     "node_modules/@mui/x-tree-view": {
-      "version": "7.3.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.3.0.tgz",
-      "integrity": "sha512-zPLtY4UP4UrglAdVRphE3Ow2UVUNKo+YkiF5z6VRqMenZBiMY+CkHSC3T+xzlAz2sSiiLZdiYJFqEpjPJI3Fcw==",
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.4.0.tgz",
+      "integrity": "sha512-gUAZ21wUbc4cpk5sAsUjZNtdryxIVgVYRYiZsz8OTzDk82JUlGmULF6Tpex93NYI+tykkrz1+/4/Tg9MIIAKUg==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.0",
         "@babel/runtime": "^7.24.0",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@types/react-transition-group": "^4.4.10",
         "@types/react-transition-group": "^4.4.10",
-        "clsx": "^2.1.0",
+        "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
         "react-transition-group": "^4.4.5"
       },
       },
@@ -1149,9 +1140,9 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@types/node": {
     "node_modules/@types/node": {
-      "version": "20.12.7",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
-      "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
+      "version": "20.12.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
+      "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "undici-types": "~5.26.4"
         "undici-types": "~5.26.4"
@@ -1168,9 +1159,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     },
     "node_modules/@types/react": {
     "node_modules/@types/react": {
-      "version": "18.2.79",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz",
-      "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==",
+      "version": "18.3.2",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
+      "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
       "dependencies": {
       "dependencies": {
         "@types/prop-types": "*",
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
         "csstype": "^3.0.2"
@@ -1184,12 +1175,6 @@
         "@types/react": "*"
         "@types/react": "*"
       }
       }
     },
     },
-    "node_modules/@types/semver": {
-      "version": "7.5.8",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
-      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
-      "dev": true
-    },
     "node_modules/@types/yargs": {
     "node_modules/@types/yargs": {
       "version": "17.0.32",
       "version": "17.0.32",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -1206,21 +1191,19 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
-      "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
+      "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "7.7.1",
-        "@typescript-eslint/type-utils": "7.7.1",
-        "@typescript-eslint/utils": "7.7.1",
-        "@typescript-eslint/visitor-keys": "7.7.1",
-        "debug": "^4.3.4",
+        "@typescript-eslint/scope-manager": "7.9.0",
+        "@typescript-eslint/type-utils": "7.9.0",
+        "@typescript-eslint/utils": "7.9.0",
+        "@typescript-eslint/visitor-keys": "7.9.0",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
         "natural-compare": "^1.4.0",
-        "semver": "^7.6.0",
         "ts-api-utils": "^1.3.0"
         "ts-api-utils": "^1.3.0"
       },
       },
       "engines": {
       "engines": {
@@ -1241,15 +1224,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
-      "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
+      "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.7.1",
-        "@typescript-eslint/types": "7.7.1",
-        "@typescript-eslint/typescript-estree": "7.7.1",
-        "@typescript-eslint/visitor-keys": "7.7.1",
+        "@typescript-eslint/scope-manager": "7.9.0",
+        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/typescript-estree": "7.9.0",
+        "@typescript-eslint/visitor-keys": "7.9.0",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
@@ -1269,13 +1252,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
-      "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
+      "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.7.1",
-        "@typescript-eslint/visitor-keys": "7.7.1"
+        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/visitor-keys": "7.9.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1286,13 +1269,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
-      "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
+      "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.7.1",
-        "@typescript-eslint/utils": "7.7.1",
+        "@typescript-eslint/typescript-estree": "7.9.0",
+        "@typescript-eslint/utils": "7.9.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
         "ts-api-utils": "^1.3.0"
       },
       },
@@ -1313,9 +1296,9 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
-      "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
+      "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1326,13 +1309,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree": {
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
-      "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
+      "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.7.1",
-        "@typescript-eslint/visitor-keys": "7.7.1",
+        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/visitor-keys": "7.9.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
@@ -1354,18 +1337,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/utils": {
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
-      "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
+      "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@types/json-schema": "^7.0.15",
-        "@types/semver": "^7.5.8",
-        "@typescript-eslint/scope-manager": "7.7.1",
-        "@typescript-eslint/types": "7.7.1",
-        "@typescript-eslint/typescript-estree": "7.7.1",
-        "semver": "^7.6.0"
+        "@typescript-eslint/scope-manager": "7.9.0",
+        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/typescript-estree": "7.9.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1379,12 +1359,12 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/visitor-keys": {
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
-      "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
+      "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/types": "7.9.0",
         "eslint-visitor-keys": "^3.4.3"
         "eslint-visitor-keys": "^3.4.3"
       },
       },
       "engines": {
       "engines": {
@@ -1667,15 +1647,15 @@
       }
       }
     },
     },
     "node_modules/ajv-formats/node_modules/ajv": {
     "node_modules/ajv-formats/node_modules/ajv": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
+      "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "fast-deep-equal": "^3.1.1",
+        "fast-deep-equal": "^3.1.3",
         "json-schema-traverse": "^1.0.0",
         "json-schema-traverse": "^1.0.0",
         "require-from-string": "^2.0.2",
         "require-from-string": "^2.0.2",
-        "uri-js": "^4.2.2"
+        "uri-js": "^4.4.1"
       },
       },
       "funding": {
       "funding": {
         "type": "github",
         "type": "github",
@@ -1997,9 +1977,9 @@
       }
       }
     },
     },
     "node_modules/caniuse-lite": {
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001612",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz",
-      "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==",
+      "version": "1.0.30001620",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz",
+      "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==",
       "dev": true,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -2345,15 +2325,15 @@
       }
       }
     },
     },
     "node_modules/electron-to-chromium": {
     "node_modules/electron-to-chromium": {
-      "version": "1.4.748",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.748.tgz",
-      "integrity": "sha512-VWqjOlPZn70UZ8FTKUOkUvBLeTQ0xpty66qV0yJcAGY2/CthI4xyW9aEozRVtuwv3Kpf5xTesmJUcPwuJmgP4A==",
+      "version": "1.4.772",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.772.tgz",
+      "integrity": "sha512-jFfEbxR/abTTJA3ci+2ok1NTuOBBtB4jH+UT6PUmRN+DY3WSD4FFRsgoVQ+QNIJ0T7wrXwzsWCI2WKC46b++2A==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/enhanced-resolve": {
     "node_modules/enhanced-resolve": {
-      "version": "5.16.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
-      "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
+      "version": "5.16.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz",
+      "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "graceful-fs": "^4.2.4",
         "graceful-fs": "^4.2.4",
@@ -2364,9 +2344,9 @@
       }
       }
     },
     },
     "node_modules/envinfo": {
     "node_modules/envinfo": {
-      "version": "7.12.0",
-      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.12.0.tgz",
-      "integrity": "sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg==",
+      "version": "7.13.0",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
+      "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
       "dev": true,
       "dev": true,
       "bin": {
       "bin": {
         "envinfo": "dist/cli.js"
         "envinfo": "dist/cli.js"
@@ -2490,9 +2470,9 @@
       }
       }
     },
     },
     "node_modules/es-module-lexer": {
     "node_modules/es-module-lexer": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz",
-      "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==",
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz",
+      "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/es-object-atoms": {
     "node_modules/es-object-atoms": {
@@ -2655,9 +2635,9 @@
       }
       }
     },
     },
     "node_modules/eslint-plugin-react-hooks": {
     "node_modules/eslint-plugin-react-hooks": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
-      "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+      "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": ">=10"
         "node": ">=10"
@@ -3201,12 +3181,13 @@
       }
       }
     },
     },
     "node_modules/globalthis": {
     "node_modules/globalthis": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
-      "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
         "node": ">= 0.4"
         "node": ">= 0.4"
@@ -4047,18 +4028,6 @@
         "loose-envify": "cli.js"
         "loose-envify": "cli.js"
       }
       }
     },
     },
-    "node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/merge-stream": {
     "node_modules/merge-stream": {
       "version": "2.0.0",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4276,17 +4245,17 @@
       }
       }
     },
     },
     "node_modules/optionator": {
     "node_modules/optionator": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
-      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
         "fast-levenshtein": "^2.0.6",
         "levn": "^0.4.1",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
         "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0"
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
       },
       },
       "engines": {
       "engines": {
         "node": ">= 0.8.0"
         "node": ">= 0.8.0"
@@ -4416,9 +4385,9 @@
       }
       }
     },
     },
     "node_modules/picocolors": {
     "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
     },
     },
     "node_modules/picomatch": {
     "node_modules/picomatch": {
       "version": "2.3.1",
       "version": "2.3.1",
@@ -4568,9 +4537,9 @@
       }
       }
     },
     },
     "node_modules/react": {
     "node_modules/react": {
-      "version": "18.2.0",
-      "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
-      "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
       "dependencies": {
       "dependencies": {
         "loose-envify": "^1.1.0"
         "loose-envify": "^1.1.0"
       },
       },
@@ -4579,15 +4548,15 @@
       }
       }
     },
     },
     "node_modules/react-dom": {
     "node_modules/react-dom": {
-      "version": "18.2.0",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
-      "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
       "dependencies": {
       "dependencies": {
         "loose-envify": "^1.1.0",
         "loose-envify": "^1.1.0",
-        "scheduler": "^0.23.0"
+        "scheduler": "^0.23.2"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "react": "^18.2.0"
+        "react": "^18.3.1"
       }
       }
     },
     },
     "node_modules/react-fast-compare": {
     "node_modules/react-fast-compare": {
@@ -4596,9 +4565,9 @@
       "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
       "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
     },
     },
     "node_modules/react-is": {
     "node_modules/react-is": {
-      "version": "18.2.0",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
-      "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
     },
     },
     "node_modules/react-transition-group": {
     "node_modules/react-transition-group": {
       "version": "4.4.5",
       "version": "4.4.5",
@@ -4834,9 +4803,9 @@
       }
       }
     },
     },
     "node_modules/scheduler": {
     "node_modules/scheduler": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
-      "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
       "dependencies": {
       "dependencies": {
         "loose-envify": "^1.1.0"
         "loose-envify": "^1.1.0"
       }
       }
@@ -4861,15 +4830,15 @@
       }
       }
     },
     },
     "node_modules/schema-utils/node_modules/ajv": {
     "node_modules/schema-utils/node_modules/ajv": {
-      "version": "8.12.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
+      "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "fast-deep-equal": "^3.1.1",
+        "fast-deep-equal": "^3.1.3",
         "json-schema-traverse": "^1.0.0",
         "json-schema-traverse": "^1.0.0",
         "require-from-string": "^2.0.2",
         "require-from-string": "^2.0.2",
-        "uri-js": "^4.2.2"
+        "uri-js": "^4.4.1"
       },
       },
       "funding": {
       "funding": {
         "type": "github",
         "type": "github",
@@ -4895,13 +4864,10 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/semver": {
     "node_modules/semver": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
-      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+      "version": "7.6.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+      "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
       "dev": true,
       "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
       "bin": {
       "bin": {
         "semver": "bin/semver.js"
         "semver": "bin/semver.js"
       },
       },
@@ -5178,9 +5144,9 @@
       }
       }
     },
     },
     "node_modules/terser": {
     "node_modules/terser": {
-      "version": "5.30.4",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz",
-      "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
+      "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "@jridgewell/source-map": "^0.3.3",
@@ -5485,9 +5451,9 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/update-browserslist-db": {
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.0.16",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
+      "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
       "dev": true,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -5504,8 +5470,8 @@
         }
         }
       ],
       ],
       "dependencies": {
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.1.2",
+        "picocolors": "^1.0.1"
       },
       },
       "bin": {
       "bin": {
         "update-browserslist-db": "cli.js"
         "update-browserslist-db": "cli.js"
@@ -5800,18 +5766,21 @@
       "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
       "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
       "dev": true
       "dev": true
     },
     },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/wrappy": {
     "node_modules/wrappy": {
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
-    },
     "node_modules/yaml": {
     "node_modules/yaml": {
       "version": "1.10.2",
       "version": "1.10.2",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",

+ 29 - 0
main.py

@@ -0,0 +1,29 @@
+from taipy.gui import Gui
+import json
+
+# Import extension library fromt the package directory name
+# from <package_dir_name> import Library
+
+default_value = 100
+
+style = {'position': 'relative', 'display': 'inline-block', 'borderRadius': '20px', 'overflow': 'hidden'}
+
+max_value = 90
+min_value = 0
+
+layout = {'paper_bgcolor': 'lavender', 'font': {'color': "darkblue", 'family': "Arial"}}
+
+page = """
+# Extension library
+<|50.54|metric|style={style}|max={max_value}|min={min_value}|threshold=70|layout={layout}|>
+"""
+
+gui = Gui(page=page)
+
+if __name__ == "__main__":
+  # Run main app
+  print("Running main app")
+  gui.run(port=8085, use_reloader=True, debug=True)
+
+  # <|{default_value}|library.metrics|delta=20|format=%.2f%%|format_delta=%.2f%%|     suffix prefix>
+  # <|{default_value}|library.metrics|delta=-20|format=%.2f%%|format_delta=%.2f%%|>

+ 48 - 2
taipy/config/common/scope.py

@@ -35,13 +35,59 @@ class _OrderedEnum(_ReprEnum):
 
 
 
 
 class Scope(_OrderedEnum):
 class Scope(_OrderedEnum):
-    """Scope of a `DataNode^`.
+    """Scope of a `DataNodeConfig^` or a `DataNode^`.
 
 
     This enumeration can have the following values:
     This enumeration can have the following values:
 
 
     - `GLOBAL`
     - `GLOBAL`
     - `CYCLE`
     - `CYCLE`
-    - `SCENARIO`
+    - `SCENARIO` (Default value)
+
+    Each data node config has a scope. It is an attribute propagated to the `DataNode^` when instantiated from
+    a `DataNodeConfig^`. The scope is used to determine the _visibility_ of the data node, and which scenarios can
+    access it.
+
+    In other words :
+
+    - There can be only one data node instantiated from a `DataNodeConfig^` with a `GLOBAL` scope. All the
+        scenarios share the unique data node. When a new scenario is created, the data node is also created if
+        and only if it does not exist yet.
+    - Only one data node instantiated from a `DataNodeConfig^` with a `CYCLE` scope is created for each cycle.
+        All the scenarios of the same cycle share the same data node. When a new scenario is created within a
+        cycle, Taipy instantiates a new data node if and only if there is no data node for the cycle yet.
+    - A data node that has the scope set to `SCENARIO` belongs to a unique scenario and cannot be used by others
+        When creating a new scenario, data nodes with a `SCENARIO` scope are systematically created along with
+        the new scenario.
+
+    !!! example
+
+        Let's consider a simple example where a company wants to predict its sales for the next month. The company
+        has a trained model that predicts the sales based on the current month and the historical sales. Based on
+        the sales forecasts the company wants to plan its production orders. The company wants to simulate two
+        scenarios every month: one with low capacity and one with high capacity.
+
+        We can create the `DataNodeConfig^`s with the following scopes:
+
+        - One data node for the historical sales with a `GLOBAL` scope.
+        - Three data nodes with a `CYCLE` scope, for the trained model, the current month, and the sales predictions.
+        - Two data nodes with a `SCENARIO` scope, for the capacity and the production orders.
+
+        The code snippet below shows how to configure the data nodes with the different scopes:
+
+        ```python
+        from taipy import Config, Scope
+
+        hist_cfg = Config.configure_csv_data_node("sales_history", scope=Scope.GLOBAL)
+        model_cfg = Config.configure_data_node("trained_model", scope=Scope.CYCLE)
+        month_cfg = Config.configure_data_node("current_month", scope=Scope.CYCLE)
+        predictions_cfg = Config.configure_data_node("sales_predictions", scope=Scope.CYCLE)
+        capacity_cfg = Config.configure_data_node("capacity", scope=Scope.SCENARIO)
+        orders_cfg = Config.configure_sql_data_node("production_orders",
+                                                    scope=Scope.SCENARIO,
+                                                    db_name="taipy",
+                                                    db_engine="sqlite",
+                                                    table_name="sales")
+        ```
     """
     """
 
 
     GLOBAL = 3
     GLOBAL = 3

+ 2 - 2
taipy/core/data/_file_datanode_mixin.py

@@ -55,8 +55,8 @@ class _FileDataNodeMixin(object):
                 Edit(
                 Edit(
                     {
                     {
                         "timestamp": self._last_edit_date,
                         "timestamp": self._last_edit_date,
-                        "writer_identifier": "TAIPY",
-                        "comments": "Default data written.",
+                        "editor": "TAIPY",
+                        "comment": "Default data written.",
                     }
                     }
                 )
                 )
             )
             )

+ 37 - 6
taipy/core/data/data_node.py

@@ -57,7 +57,7 @@ def _update_ready_for_reading(fct):
 class DataNode(_Entity, _Labeled):
 class DataNode(_Entity, _Labeled):
     """Reference to a dataset.
     """Reference to a dataset.
 
 
-    A Data Node is an abstract class that holds metadata related to the dataset it refers to.
+    A Data Node is an abstract class that holds metadata related to the data it refers to.
     In particular, a data node holds the name, the scope, the owner identifier, the last
     In particular, a data node holds the name, the scope, the owner identifier, the last
     edit date, and some additional properties of the data.<br/>
     edit date, and some additional properties of the data.<br/>
     A Data Node also contains information and methods needed to access the dataset. This
     A Data Node also contains information and methods needed to access the dataset. This
@@ -65,7 +65,37 @@ class DataNode(_Entity, _Labeled):
     SQL Data Node, CSV Data Node, ...).
     SQL Data Node, CSV Data Node, ...).
 
 
     !!! note
     !!! note
-        It is recommended not to instantiate subclasses of `DataNode` directly.
+        It is not recommended to instantiate subclasses of `DataNode` directly. Instead,
+        you have two ways:
+
+        1. Create a Scenario using the `create_scenario()^` function. Related data nodes
+            will be created automatically. Please refer to the `Scenario^` class for more
+            information.
+        2. Configure a `DataNodeConfig^` with the various configuration methods form `Config^`
+            and use the `create_global_data_node()^` function as illustrated in the following
+            example.
+
+    !!! Example
+
+        ```python
+        import taipy as tp
+        from taipy import Config
+
+        # Configure a global data node
+        dataset_cfg = Config.configure_data_node("my_dataset", scope=tp.Scope.GLOBAL)
+
+        # Instantiate a global data node
+        dataset = tp.create_global_data_node(dataset_cfg)
+
+        # Retrieve the list of all data nodes
+        all_data_nodes = tp.get_data_nodes()
+
+        # Write the data
+        dataset.write("Hello, World!")
+
+        # Read the data
+        print(dataset.read())
+        ```
 
 
     Attributes:
     Attributes:
         config_id (str): Identifier of the data node configuration. It must be a valid Python
         config_id (str): Identifier of the data node configuration. It must be a valid Python
@@ -78,10 +108,11 @@ class DataNode(_Entity, _Labeled):
         parent_ids (Optional[Set[str]]): The set of identifiers of the parent tasks.
         parent_ids (Optional[Set[str]]): The set of identifiers of the parent tasks.
         last_edit_date (datetime): The date and time of the last modification.
         last_edit_date (datetime): The date and time of the last modification.
         edits (List[Edit^]): The list of Edits (an alias for dict) containing metadata about each
         edits (List[Edit^]): The list of Edits (an alias for dict) containing metadata about each
-            data edition including but not limited to timestamp, comments, job_id:
-            timestamp: The time instant of the writing
-            comments: Representation of a free text to explain or comment on a data change
-            job_id: Only populated when the data node is written by a task execution and corresponds to the job's id.
+            data edition including but not limited to:
+                <ul><li>timestamp: The time instant of the writing </li>
+                <li>comments: Representation of a free text to explain or comment on a data change</li>
+                <li>job_id: Only populated when the data node is written by a task execution and
+                    corresponds to the job's id.</li></ul>
             Additional metadata related to the edition made to the data node can also be provided in Edits.
             Additional metadata related to the edition made to the data node can also be provided in Edits.
         version (str): The string indicates the application version of the data node to
         version (str): The string indicates the application version of the data node to
             instantiate. If not provided, the current version is used.
             instantiate. If not provided, the current version is used.

+ 2 - 2
taipy/core/data/in_memory.py

@@ -101,8 +101,8 @@ class InMemoryDataNode(DataNode):
                 Edit(
                 Edit(
                     {
                     {
                         "timestamp": self._last_edit_date,
                         "timestamp": self._last_edit_date,
-                        "writer_identifier": "TAIPY",
-                        "comments": "Default data written.",
+                        "editor": "TAIPY",
+                        "comment": "Default data written.",
                     }
                     }
                 )
                 )
             )
             )

+ 34 - 0
taipy/core/scenario/scenario.py

@@ -55,6 +55,40 @@ class Scenario(_Entity, Submittable, _Labeled):
     solve the Business case. It also holds a set of additional data nodes (instances of `DataNode` class)
     solve the Business case. It also holds a set of additional data nodes (instances of `DataNode` class)
     for extra data related to the scenario.
     for extra data related to the scenario.
 
 
+    !!! note
+
+        It is not recommended to instantiate a `Scenario` directly. Instead, it should be
+        created with the `create_scenario()^` function.
+
+    !!! Example
+
+        ```python
+        import taipy as tp
+        from taipy import Config
+
+        def by_two(x: int):
+            return x * 2
+
+        # Configure scenarios
+        input_cfg = Config.configure_data_node("my_input")
+        result_cfg = Config.configure_data_node("my_result")
+        task_cfg = Config.configure_task("my_double", function=by_two, input=input_cfg, output=result_cfg)
+        scenario_cfg = Config.configure_scenario("my_scenario", task_configs=[task_cfg])
+
+        # Create a new scenario from the configuration
+        scenario = tp.create_scenario(scenario_cfg)
+
+        # Write the input data and submit the scenario
+        scenario.my_input.write(3)
+        scenario.submit()
+
+        # Read the result
+        print(scenario.my_result.read())  # Output: 6
+
+        # Retrieve all scenarios
+        all_scenarios = tp.get_scenarios()
+        ```
+
     Attributes:
     Attributes:
         config_id (str): The identifier of the `ScenarioConfig^`.
         config_id (str): The identifier of the `ScenarioConfig^`.
         tasks (Set[Task^]): The set of tasks.
         tasks (Set[Task^]): The set of tasks.

+ 40 - 0
taipy/core/task/task.py

@@ -33,6 +33,46 @@ class Task(_Entity, _Labeled):
     A `Task` brings together the user code as function, the inputs and the outputs as data nodes
     A `Task` brings together the user code as function, the inputs and the outputs as data nodes
     (instances of the `DataNode^` class).
     (instances of the `DataNode^` class).
 
 
+    !!! note
+        It is not recommended to instantiate a `Task` directly. Instead, it should be
+        created with the `create_scenario()^` function. When creating a `Scenario^`,
+        the related data nodes and tasks are created automatically. Please refer to
+        the `Scenario^` class for more information.
+
+    !!! Example
+
+        ```python
+        import taipy as tp
+        from taipy import Config
+
+        def by_two(x: int):
+            return x * 2
+
+        # Configure data nodes, tasks and scenarios
+        input_cfg = Config.configure_data_node("my_input", default_data=2)
+        result_cfg = Config.configure_data_node("my_result")
+        task_cfg = Config.configure_task("my_double", function=by_two, input=input_cfg, output=result_cfg)
+        scenario_cfg = Config.configure_scenario("my_scenario", task_configs=[task_cfg])
+
+        # Instantiate a task along with a scenario
+        sc = tp.create_scenario(scenario_cfg)
+
+        # Retrieve task and data nodes from scenario
+        task_input = sc.my_input
+        double_task = sc.my_double
+        task_result = sc.my_result
+
+        # Write the input data and submit the task
+        task_input.write(3)
+        double_task.submit()
+
+        # Read the result
+        print(task_result.read())  # Output: 6
+
+        # Retrieve the list of all tasks
+        all_tasks = tp.get_tasks()
+        ```
+
     Attributes:
     Attributes:
         config_id (str): The identifier of the `TaskConfig^`.
         config_id (str): The identifier of the `TaskConfig^`.
         properties (dict[str, Any]): A dictionary of additional properties.
         properties (dict[str, Any]): A dictionary of additional properties.

+ 19 - 5
taipy/gui/_renderers/__init__.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # 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.
 # specific language governing permissions and limitations under the License.
 
 
+import re
 import typing as t
 import typing as t
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from os import path
 from os import path
@@ -22,7 +23,7 @@ from ..utils import _is_in_notebook, _varname_from_content
 from ._html import _TaipyHTMLParser
 from ._html import _TaipyHTMLParser
 
 
 if t.TYPE_CHECKING:
 if t.TYPE_CHECKING:
-    from watchdog.observers import BaseObserverSubclassCallable
+    from watchdog.observers.api import BaseObserver
 
 
     from ..gui import Gui
     from ..gui import Gui
 
 
@@ -46,7 +47,8 @@ class _Renderer(Page, ABC):
         self._content = ""
         self._content = ""
         self._base_element: t.Optional[_Element] = None
         self._base_element: t.Optional[_Element] = None
         self._filepath = ""
         self._filepath = ""
-        self._observer: t.Optional["BaseObserverSubclassCallable"] = None
+        self._observer: t.Optional["BaseObserver"] = None
+        self._encoding: t.Optional[str] = kwargs.get("encoding", None)
         if isinstance(content, str):
         if isinstance(content, str):
             self.__process_content(content)
             self.__process_content(content)
         elif isinstance(content, _Element):
         elif isinstance(content, _Element):
@@ -68,7 +70,7 @@ class _Renderer(Page, ABC):
             if _is_in_notebook() and self._observer is None:
             if _is_in_notebook() and self._observer is None:
                 self.__observe_file_change(content)
                 self.__observe_file_change(content)
             return
             return
-        self._content = content
+        self._content = self.__sanitize_content(content)
 
 
     def __observe_file_change(self, file_path: str):
     def __observe_file_change(self, file_path: str):
         from watchdog.observers import Observer
         from watchdog.observers import Observer
@@ -84,13 +86,25 @@ class _Renderer(Page, ABC):
         with open(t.cast(str, content), "rb") as f:
         with open(t.cast(str, content), "rb") as f:
             file_content = f.read()
             file_content = f.read()
             encoding = "utf-8"
             encoding = "utf-8"
-            if (detected_encoding := detect(file_content)["encoding"]) is not None:
+            if self._encoding is not None:
+                encoding = self._encoding
+                _TaipyLogger._get_logger().info(f"'{encoding}' encoding was used to decode file '{content}'.")
+            elif (detected_encoding := detect(file_content)["encoding"]) is not None:
                 encoding = detected_encoding
                 encoding = detected_encoding
                 _TaipyLogger._get_logger().info(f"Detected '{encoding}' encoding for file '{content}'.")
                 _TaipyLogger._get_logger().info(f"Detected '{encoding}' encoding for file '{content}'.")
-            self._content = file_content.decode(encoding)
+            else:
+                _TaipyLogger._get_logger().info(f"Using default '{encoding}' encoding for file '{content}'.")
+            self._content = self.__sanitize_content(file_content.decode(encoding))
             # Save file path for error handling
             # Save file path for error handling
             self._filepath = content
             self._filepath = content
 
 
+    def __sanitize_content(self, content: str) -> str:
+        # replace all CRLF (\r\n) with LF (\n)
+        text = re.sub(r'\r\n', '\n', content)
+        # replace all remaining CR (\r) with LF (\n)
+        text = re.sub(r'\r', '\n', content)
+        return text
+
     def set_content(self, content: str) -> None:
     def set_content(self, content: str) -> None:
         """Set a new page content.
         """Set a new page content.
 
 

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

@@ -31,6 +31,7 @@ class _Factory:
 
 
     __CONTROL_DEFAULT_PROP_NAME = {
     __CONTROL_DEFAULT_PROP_NAME = {
         "button": "label",
         "button": "label",
+        "chat": "messages",
         "chart": "data",
         "chart": "data",
         "content": "value",
         "content": "value",
         "date": "date",
         "date": "date",
@@ -82,6 +83,23 @@ class _Factory:
                 ("hover_text", PropertyType.dynamic_string),
                 ("hover_text", PropertyType.dynamic_string),
             ]
             ]
         ),
         ),
+        "chat": lambda gui, control_type, attrs: _Builder(
+            gui=gui, control_type=control_type, element_name="Chat", attributes=attrs, default_value=None
+        )
+        .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data)
+        .set_attributes(
+            [
+                ("id",),
+                ("on_action", PropertyType.function),
+                ("active", PropertyType.dynamic_boolean, True),
+                ("hover_text", PropertyType.dynamic_string),
+                ("with_input", PropertyType.dynamic_boolean, True),
+                ("users", PropertyType.lov),
+                ("sender_id",),
+                ("height",),
+                ("page_size", PropertyType.number, 50),
+            ]
+        ),
         "chart": lambda gui, control_type, attrs: _Builder(
         "chart": lambda gui, control_type, attrs: _Builder(
             gui=gui, control_type=control_type, element_name="Chart", attributes=attrs, default_value=None
             gui=gui, control_type=control_type, element_name="Chart", attributes=attrs, default_value=None
         )
         )

+ 9 - 1
taipy/gui/data/data_scope.py

@@ -16,13 +16,17 @@ from types import SimpleNamespace
 
 
 from .._warnings import _warn
 from .._warnings import _warn
 
 
+if t.TYPE_CHECKING:
+    from ..gui import Gui
+
 
 
 class _DataScopes:
 class _DataScopes:
     _GLOBAL_ID = "global"
     _GLOBAL_ID = "global"
     _META_PRE_RENDER = "pre_render"
     _META_PRE_RENDER = "pre_render"
     _DEFAULT_METADATA = {_META_PRE_RENDER: False}
     _DEFAULT_METADATA = {_META_PRE_RENDER: False}
 
 
-    def __init__(self) -> None:
+    def __init__(self, gui: "Gui") -> None:
+        self.__gui = gui
         self.__scopes: t.Dict[str, SimpleNamespace] = {_DataScopes._GLOBAL_ID: SimpleNamespace()}
         self.__scopes: t.Dict[str, SimpleNamespace] = {_DataScopes._GLOBAL_ID: SimpleNamespace()}
         # { scope_name: { metadata: value } }
         # { scope_name: { metadata: value } }
         self.__scopes_metadata: t.Dict[str, t.Dict[str, t.Any]] = {
         self.__scopes_metadata: t.Dict[str, t.Dict[str, t.Any]] = {
@@ -63,6 +67,10 @@ class _DataScopes:
         if id not in self.__scopes:
         if id not in self.__scopes:
             self.__scopes[id] = SimpleNamespace()
             self.__scopes[id] = SimpleNamespace()
             self.__scopes_metadata[id] = _DataScopes._DEFAULT_METADATA.copy()
             self.__scopes_metadata[id] = _DataScopes._DEFAULT_METADATA.copy()
+            # Propagate shared variables to the new scope from the global scope
+            for var in self.__gui._get_shared_variables():
+                if hasattr(self.__scopes[_DataScopes._GLOBAL_ID], var):
+                    setattr(self.__scopes[id], var, getattr(self.__scopes[_DataScopes._GLOBAL_ID], var, None))
 
 
     def delete_scope(self, id: str) -> None:  # pragma: no cover
     def delete_scope(self, id: str) -> None:  # pragma: no cover
         if self.__single_client:
         if self.__single_client:

+ 8 - 0
taipy/gui/data/pandas_data_accessor.py

@@ -313,6 +313,14 @@ class _PandasDataAccessor(_DataAccessor):
                 start = 0
                 start = 0
             if end < 0 or end >= rowcount:
             if end < 0 or end >= rowcount:
                 end = rowcount - 1
                 end = rowcount - 1
+            if payload.get("reverse", False):
+                diff = end - start
+                end = rowcount - 1 - start
+                if end < 0:
+                    end = rowcount - 1
+                start = end - diff
+                if start < 0:
+                    start = 0
             # deal with sort
             # deal with sort
             order_by = payload.get("orderby")
             order_by = payload.get("orderby")
             if isinstance(order_by, str) and len(order_by):
             if isinstance(order_by, str) and len(order_by):

+ 11 - 4
taipy/gui/gui.py

@@ -2275,12 +2275,19 @@ class Gui:
 
 
     def __init_ngrok(self):
     def __init_ngrok(self):
         app_config = self._config.config
         app_config = self._config.config
-        if app_config["run_server"] and app_config["ngrok_token"]:  # pragma: no cover
+        if hasattr(self, "_ngrok"):
+            # Keep the ngrok instance if token has not changed
+            if app_config["ngrok_token"] == self._ngrok[1]:
+                _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
+                return
+            # Close the old tunnel so new tunnel can open for new token
+            ngrok.disconnect(self._ngrok[0].public_url)
+        if app_config["run_server"] and (token := app_config["ngrok_token"]):  # pragma: no cover
             if not util.find_spec("pyngrok"):
             if not util.find_spec("pyngrok"):
                 raise RuntimeError("Cannot use ngrok as pyngrok package is not installed.")
                 raise RuntimeError("Cannot use ngrok as pyngrok package is not installed.")
-            ngrok.set_auth_token(app_config["ngrok_token"])
-            http_tunnel = ngrok.connect(app_config["port"], "http")
-            _TaipyLogger._get_logger().info(f" * NGROK Public Url: {http_tunnel.public_url}")
+            ngrok.set_auth_token(token)
+            self._ngrok = (ngrok.connect(app_config["port"], "http"), token)
+            _TaipyLogger._get_logger().info(f" * NGROK Public Url: {self._ngrok[0].public_url}")
 
 
     def __bind_default_function(self):
     def __bind_default_function(self):
         with self.get_flask_app().app_context():
         with self.get_flask_app().app_context():

+ 2 - 2
taipy/gui/gui_actions.py

@@ -322,9 +322,9 @@ def invoke_state_callback(gui: Gui, state_id: str, callback: t.Callable, args: t
 def invoke_long_callback(
 def invoke_long_callback(
     state: State,
     state: State,
     user_function: t.Callable,
     user_function: t.Callable,
-    user_function_args: t.Union[t.Tuple, t.List] = None,
+    user_function_args: t.Optional[t.Union[t.Tuple, t.List]] = None,
     user_status_function: t.Optional[t.Callable] = None,
     user_status_function: t.Optional[t.Callable] = None,
-    user_status_function_args: t.Union[t.Tuple, t.List] = None,
+    user_status_function_args: t.Optional[t.Union[t.Tuple, t.List]] = None,
     period=0,
     period=0,
 ):
 ):
     """Invoke a long running user callback.
     """Invoke a long running user callback.

+ 2 - 2
taipy/gui/utils/_bindings.py

@@ -23,7 +23,7 @@ if t.TYPE_CHECKING:
 class _Bindings:
 class _Bindings:
     def __init__(self, gui: "Gui") -> None:
     def __init__(self, gui: "Gui") -> None:
         self.__gui = gui
         self.__gui = gui
-        self.__scopes = _DataScopes()
+        self.__scopes = _DataScopes(gui)
 
 
     def _bind(self, name: str, value: t.Any) -> None:
     def _bind(self, name: str, value: t.Any) -> None:
         if hasattr(self, name):
         if hasattr(self, name):
@@ -69,7 +69,7 @@ class _Bindings:
         return id, create
         return id, create
 
 
     def _new_scopes(self):
     def _new_scopes(self):
-        self.__scopes = _DataScopes()
+        self.__scopes = _DataScopes(self.__gui)
 
 
     def _get_data_scope(self):
     def _get_data_scope(self):
         return self.__scopes.get_scope(self.__gui._get_client_id())[0]
         return self.__scopes.get_scope(self.__gui._get_client_id())[0]

+ 69 - 0
tests/gui/builder/control/test_chat.py

@@ -0,0 +1,69 @@
+# 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 taipy.gui.builder as tgb
+from taipy.gui import Gui, Icon
+
+
+def test_chat_builder_1(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+
+    gui._bind_var_val(
+        "chat_properties",
+        {"users": [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]},
+    )
+
+    with tgb.Page(frame=None) as page:
+        tgb.chat(messages="{messages}", properties="{chat_properties}")  # type: ignore[attr-defined]
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="tpec_TpExPr_messages_TPMDL_0"',
+    ]
+    helpers.test_control_builder(gui, page, expected_list)
+
+
+def test_chat_builder_2(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+
+    gui._bind_var_val(
+        "users", [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]
+    )
+
+    with tgb.Page(frame=None) as page:
+        tgb.chat(messages="{messages}", users="{users}")  # type: ignore[attr-defined]
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="tpec_TpExPr_messages_TPMDL_0"',
+        "users={_TpL_tpec_TpExPr_users_TPMDL_0}",
+        'updateVars="users=_TpL_tpec_TpExPr_users_TPMDL_0"'
+    ]
+    helpers.test_control_builder(gui, page, expected_list)
+

+ 175 - 0
tests/gui/control/test_chat.py

@@ -0,0 +1,175 @@
+# 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 taipy.gui import Gui, Icon
+
+
+def test_chat_md_1(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "chat_properties",
+        {
+            "users": [
+                ["Fred", Icon("/images/favicon.png", "Fred.png")],
+                ["Fredi", Icon("/images/fred.png", "Fred.png")],
+            ],
+            "sender_id": "sender",
+            "on_action": "on_action",
+            "with_input": False,
+            "height": "50vh",
+        },
+    )
+    md_string = "<|{messages}|chat|properties=chat_properties|>"
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+        'senderId="sender"',
+        'onAction="on_action"',
+        "defaultWithInput={false}",
+        'height="50vh"',
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
+def test_chat_md_2(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "users", [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]
+    )
+    gui._bind_var_val("winp", False)
+    md_string = "<|{messages}|chat|users={users}|with_input={winp}|>"
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+        'updateVars="users=_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0"',
+        "users={_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0}",
+        "withInput={_TpB_tpec_TpExPr_winp_TPMDL_0}>",
+        "defaultWithInput={false}",
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
+def test_chat_html_1_1(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "chat_properties",
+        {"users": [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]},
+    )
+    html_string = '<taipy:chat messages="{messages}" properties="chat_properties"/>'
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+    ]
+    helpers.test_control_html(gui, html_string, expected_list)
+
+
+def test_chat_html_1_2(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "chat_properties",
+        {"users": [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]},
+    )
+    html_string = '<taipy:chat properties="chat_properties">{messages}</taipy:chat>'
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+    ]
+    helpers.test_control_html(gui, html_string, expected_list)
+
+
+def test_chat_html_2_1(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "users", [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]
+    )
+    html_string = '<taipy:chat messages="{messages}" users="{users}" />'
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+        'updateVars="users=_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0"',
+        "users={_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0}",
+    ]
+    helpers.test_control_html(gui, html_string, expected_list)
+
+
+def test_chat_html_2_2(gui: Gui, test_client, helpers):
+    gui._bind_var_val(
+        "messages",
+        [
+            ["1", "msg 1", "Fred"],
+            ["2", "msg From Another unknown User", "Fredo"],
+            ["3", "This from the sender User", "taipy"],
+            ["4", "And from another known one", "Fredi"],
+        ],
+    )
+    gui._bind_var_val(
+        "users", [["Fred", Icon("/images/favicon.png", "Fred.png")], ["Fredi", Icon("/images/fred.png", "Fred.png")]]
+    )
+    html_string = '<taipy:chat users="{users}">{messages}</taipy:chat>'
+    expected_list = [
+        "<Chat",
+        'defaultUsers="[[&quot;Fred&quot;, &#x7B;&quot;path&quot;: &quot;/images/favicon.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;], [&quot;Fredi&quot;, &#x7B;&quot;path&quot;: &quot;/images/fred.png&quot;, &quot;text&quot;: &quot;Fred.png&quot;&#x7D;]]"',  # noqa: E501
+        "messages={_TpD_tpec_TpExPr_messages_TPMDL_0}",
+        'updateVarName="_TpD_tpec_TpExPr_messages_TPMDL_0"',
+        'updateVars="users=_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0"',
+        "users={_TpL_tp_TpExPr_gui_get_adapted_lov_users_list_TPMDL_0_0}",
+    ]
+    helpers.test_control_html(gui, html_string, expected_list)

+ 0 - 0
tests/gui/e2e/__init__.py


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно