浏览代码

merge develop intoUS551

namnguyen 1 年之前
父节点
当前提交
83c0692536

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

@@ -11,11 +11,11 @@
  * 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 = {
  // testEnvironment: 'jest-environment-jsdom',
   preset: 'ts-jest',
   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"],
 };

文件差异内容过多而无法显示
+ 216 - 311
frontend/taipy-gui/package-lock.json


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

@@ -74,7 +74,7 @@
   },
   "devDependencies": {
     "@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",
     "@types/css-mediaquery": "^0.1.1",
     "@types/jest": "^29.0.1",
@@ -94,11 +94,11 @@
     "autoprefixer": "^10.4.0",
     "copy-webpack-plugin": "^12.0.1",
     "cross-env": "^7.0.3",
-    "css-loader": "^6.5.0",
+    "css-loader": "^7.1.0",
     "css-mediaquery": "^0.1.2",
     "dotenv-webpack": "^8.0.0",
     "dts-bundle-generator": "^9.2.1",
-    "eslint": "^8.3.0",
+    "eslint": "^8.57.0",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react-hooks": "^4.2.0",
     "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 Button from "./Button";
+import Chat from "./Chat";
 import Chart from "./Chart";
 import DateRange from "./DateRange";
 import DateSelector from "./DateSelector";
@@ -47,6 +48,7 @@ export const getRegisteredComponents = () => {
         Object.entries({
             a: Link,
             Button: Button,
+            Chat: Chat,
             Chart: Chart,
             DateRange: DateRange,
             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(" ");
 
 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];
         }
         return pv;
-    }, {} as typeof payload)
-}
+    }, {} as typeof payload);
+};
 
 export const createRequestTableUpdateAction = (
     name: string | undefined,
@@ -605,21 +605,28 @@ export const createRequestTableUpdateAction = (
     compareDatas?: string,
     stateContext?: Record<string, unknown>
 ): 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 = (
     name: string | undefined,
@@ -639,24 +646,33 @@ export const createRequestInfiniteTableUpdateAction = (
     filters?: Array<FilterDesc>,
     compare?: string,
     compareDatas?: string,
-    stateContext?: Record<string, unknown>
+    stateContext?: Record<string, unknown>,
+    reverse?: boolean
 ): 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`.

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

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

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

@@ -11,7 +11,7 @@
  * 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 { getUpdateVars } from "../components/Taipy/utils";
@@ -92,7 +92,7 @@ export const useDispatchRequestUpdateOnFirstRender = (
     forceRefresh?: boolean
 ) => {
     useEffect(() => {
-        const updateArray = getUpdateVars(updateVars).filter(uv => !uv.includes(","));
+        const updateArray = getUpdateVars(updateVars).filter((uv) => !uv.includes(","));
         varName && updateArray.push(varName);
         updateArray.length && dispatch(createRequestUpdateAction(id, context, updateArray, 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 => {
     // Get a mutable ref object where we can store props ...
     // ... for comparison next time this hook runs.
-    const previousProps = useRef({} as Record<string, unknown>);
+    const previousProps = useRef<Record<string, unknown>>();
     useEffect(() => {
         if (previousProps.current) {
             // Get all keys from previous and current props
@@ -167,7 +167,7 @@ export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>)
             // Iterate through keys
             allKeys.forEach((key) => {
                 // If previous is different from current
-                if (previousProps.current[key] !== props[key]) {
+                if (previousProps.current && previousProps.current[key] !== props[key]) {
                     // Add to changesObj
                     changesObj[key] = {
                         from: previousProps.current[key],
@@ -184,3 +184,24 @@ export const useWhyDidYouUpdate = (name: string, props: Record<string, unknown>)
         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 axios from "axios";
 import Avatar from "@mui/material/Avatar";
-import { SxProps, useTheme, Theme } from "@mui/system";
+import { SxProps, useTheme, Theme } from "@mui/material/styles";
 
 /**
  * An Icon representation.
@@ -39,7 +39,7 @@ interface IconProps {
     id?: string;
     img: Icon;
     className?: string;
-    sx?: SxProps;
+    sx?: SxProps<Theme>;
 }
 
 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": {
       "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": {
       "version": "7.24.2",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
@@ -84,19 +75,19 @@
       }
     },
     "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": {
         "node": ">=6.9.0"
       }
     },
     "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": {
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "chalk": "^2.4.2",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
@@ -170,9 +161,9 @@
       }
     },
     "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": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -181,12 +172,12 @@
       }
     },
     "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": {
-        "@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"
       },
       "engines": {
@@ -414,28 +405,28 @@
       }
     },
     "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": {
-        "@floating-ui/utils": "^0.2.1"
+        "@floating-ui/utils": "^0.2.0"
       }
     },
     "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": {
         "@floating-ui/core": "^1.0.0",
         "@floating-ui/utils": "^0.2.0"
       }
     },
     "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": {
-        "@floating-ui/dom": "^1.6.1"
+        "@floating-ui/dom": "^1.0.0"
       },
       "peerDependencies": {
         "react": ">=16.8.0",
@@ -443,9 +434,9 @@
       }
     },
     "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": {
       "version": "0.11.14",
@@ -652,18 +643,18 @@
       }
     },
     "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": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
       }
     },
     "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": {
         "@babel/runtime": "^7.23.9"
       },
@@ -686,13 +677,13 @@
       }
     },
     "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": {
         "@babel/runtime": "^7.23.9",
         "@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/types": "^7.2.14",
         "@mui/utils": "^5.15.14",
@@ -866,16 +857,16 @@
       }
     },
     "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": {
         "@babel/runtime": "^7.24.0",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@types/react-transition-group": "^4.4.10",
-        "clsx": "^2.1.0",
+        "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
       },
@@ -891,7 +882,7 @@
         "@emotion/styled": "^11.8.1",
         "@mui/material": "^5.15.14",
         "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",
         "luxon": "^3.0.2",
         "moment": "^2.29.4",
@@ -931,16 +922,16 @@
       }
     },
     "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": {
         "@babel/runtime": "^7.24.0",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@types/react-transition-group": "^4.4.10",
-        "clsx": "^2.1.0",
+        "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
       },
@@ -1149,9 +1140,9 @@
       "dev": true
     },
     "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,
       "dependencies": {
         "undici-types": "~5.26.4"
@@ -1168,9 +1159,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     "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": {
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
@@ -1184,12 +1175,6 @@
         "@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": {
       "version": "17.0.32",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -1206,21 +1191,19 @@
       "dev": true
     },
     "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,
       "dependencies": {
         "@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",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
-        "semver": "^7.6.0",
         "ts-api-utils": "^1.3.0"
       },
       "engines": {
@@ -1241,15 +1224,15 @@
       }
     },
     "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,
       "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"
       },
       "engines": {
@@ -1269,13 +1252,13 @@
       }
     },
     "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,
       "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": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1286,13 +1269,13 @@
       }
     },
     "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,
       "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",
         "ts-api-utils": "^1.3.0"
       },
@@ -1313,9 +1296,9 @@
       }
     },
     "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,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1326,13 +1309,13 @@
       }
     },
     "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,
       "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",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -1354,18 +1337,15 @@
       }
     },
     "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,
       "dependencies": {
         "@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": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1379,12 +1359,12 @@
       }
     },
     "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,
       "dependencies": {
-        "@typescript-eslint/types": "7.7.1",
+        "@typescript-eslint/types": "7.9.0",
         "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
@@ -1667,15 +1647,15 @@
       }
     },
     "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,
       "dependencies": {
-        "fast-deep-equal": "^3.1.1",
+        "fast-deep-equal": "^3.1.3",
         "json-schema-traverse": "^1.0.0",
         "require-from-string": "^2.0.2",
-        "uri-js": "^4.2.2"
+        "uri-js": "^4.4.1"
       },
       "funding": {
         "type": "github",
@@ -1997,9 +1977,9 @@
       }
     },
     "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,
       "funding": [
         {
@@ -2345,15 +2325,15 @@
       }
     },
     "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
     },
     "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,
       "dependencies": {
         "graceful-fs": "^4.2.4",
@@ -2364,9 +2344,9 @@
       }
     },
     "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,
       "bin": {
         "envinfo": "dist/cli.js"
@@ -2490,9 +2470,9 @@
       }
     },
     "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
     },
     "node_modules/es-object-atoms": {
@@ -2655,9 +2635,9 @@
       }
     },
     "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,
       "engines": {
         "node": ">=10"
@@ -3201,12 +3181,13 @@
       }
     },
     "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,
       "dependencies": {
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -4047,18 +4028,6 @@
         "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": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4276,17 +4245,17 @@
       }
     },
     "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,
       "dependencies": {
-        "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0"
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
       },
       "engines": {
         "node": ">= 0.8.0"
@@ -4416,9 +4385,9 @@
       }
     },
     "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": {
       "version": "2.3.1",
@@ -4568,9 +4537,9 @@
       }
     },
     "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": {
         "loose-envify": "^1.1.0"
       },
@@ -4579,15 +4548,15 @@
       }
     },
     "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": {
         "loose-envify": "^1.1.0",
-        "scheduler": "^0.23.0"
+        "scheduler": "^0.23.2"
       },
       "peerDependencies": {
-        "react": "^18.2.0"
+        "react": "^18.3.1"
       }
     },
     "node_modules/react-fast-compare": {
@@ -4596,9 +4565,9 @@
       "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
     },
     "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": {
       "version": "4.4.5",
@@ -4834,9 +4803,9 @@
       }
     },
     "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": {
         "loose-envify": "^1.1.0"
       }
@@ -4861,15 +4830,15 @@
       }
     },
     "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,
       "dependencies": {
-        "fast-deep-equal": "^3.1.1",
+        "fast-deep-equal": "^3.1.3",
         "json-schema-traverse": "^1.0.0",
         "require-from-string": "^2.0.2",
-        "uri-js": "^4.2.2"
+        "uri-js": "^4.4.1"
       },
       "funding": {
         "type": "github",
@@ -4895,13 +4864,10 @@
       "dev": true
     },
     "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,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -5178,9 +5144,9 @@
       }
     },
     "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,
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
@@ -5485,9 +5451,9 @@
       "dev": true
     },
     "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,
       "funding": [
         {
@@ -5504,8 +5470,8 @@
         }
       ],
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.1.2",
+        "picocolors": "^1.0.1"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -5800,18 +5766,21 @@
       "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
       "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": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "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": {
       "version": "1.10.2",
       "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):
-    """Scope of a `DataNode^`.
+    """Scope of a `DataNodeConfig^` or a `DataNode^`.
 
     This enumeration can have the following values:
 
     - `GLOBAL`
     - `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

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

@@ -55,8 +55,8 @@ class _FileDataNodeMixin(object):
                 Edit(
                     {
                         "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):
     """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
     edit date, and some additional properties of the data.<br/>
     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, ...).
 
     !!! 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:
         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.
         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
-            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.
         version (str): The string indicates the application version of the data node to
             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(
                     {
                         "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)
     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:
         config_id (str): The identifier of the `ScenarioConfig^`.
         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
     (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:
         config_id (str): The identifier of the `TaskConfig^`.
         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
 # specific language governing permissions and limitations under the License.
 
+import re
 import typing as t
 from abc import ABC, abstractmethod
 from os import path
@@ -22,7 +23,7 @@ from ..utils import _is_in_notebook, _varname_from_content
 from ._html import _TaipyHTMLParser
 
 if t.TYPE_CHECKING:
-    from watchdog.observers import BaseObserverSubclassCallable
+    from watchdog.observers.api import BaseObserver
 
     from ..gui import Gui
 
@@ -46,7 +47,8 @@ class _Renderer(Page, ABC):
         self._content = ""
         self._base_element: t.Optional[_Element] = None
         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):
             self.__process_content(content)
         elif isinstance(content, _Element):
@@ -68,7 +70,7 @@ class _Renderer(Page, ABC):
             if _is_in_notebook() and self._observer is None:
                 self.__observe_file_change(content)
             return
-        self._content = content
+        self._content = self.__sanitize_content(content)
 
     def __observe_file_change(self, file_path: str):
         from watchdog.observers import Observer
@@ -84,13 +86,25 @@ class _Renderer(Page, ABC):
         with open(t.cast(str, content), "rb") as f:
             file_content = f.read()
             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
                 _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
             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:
         """Set a new page content.
 

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

@@ -31,6 +31,7 @@ class _Factory:
 
     __CONTROL_DEFAULT_PROP_NAME = {
         "button": "label",
+        "chat": "messages",
         "chart": "data",
         "content": "value",
         "date": "date",
@@ -82,6 +83,23 @@ class _Factory:
                 ("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(
             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
 
+if t.TYPE_CHECKING:
+    from ..gui import Gui
+
 
 class _DataScopes:
     _GLOBAL_ID = "global"
     _META_PRE_RENDER = "pre_render"
     _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()}
         # { scope_name: { metadata: value } }
         self.__scopes_metadata: t.Dict[str, t.Dict[str, t.Any]] = {
@@ -63,6 +67,10 @@ class _DataScopes:
         if id not in self.__scopes:
             self.__scopes[id] = SimpleNamespace()
             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
         if self.__single_client:

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

@@ -313,6 +313,14 @@ class _PandasDataAccessor(_DataAccessor):
                 start = 0
             if end < 0 or end >= rowcount:
                 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
             order_by = payload.get("orderby")
             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):
         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"):
                 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):
         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(
     state: State,
     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_args: t.Union[t.Tuple, t.List] = None,
+    user_status_function_args: t.Optional[t.Union[t.Tuple, t.List]] = None,
     period=0,
 ):
     """Invoke a long running user callback.

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

@@ -23,7 +23,7 @@ if t.TYPE_CHECKING:
 class _Bindings:
     def __init__(self, gui: "Gui") -> None:
         self.__gui = gui
-        self.__scopes = _DataScopes()
+        self.__scopes = _DataScopes(gui)
 
     def _bind(self, name: str, value: t.Any) -> None:
         if hasattr(self, name):
@@ -69,7 +69,7 @@ class _Bindings:
         return id, create
 
     def _new_scopes(self):
-        self.__scopes = _DataScopes()
+        self.__scopes = _DataScopes(self.__gui)
 
     def _get_data_scope(self):
         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


部分文件因为文件数量过多而无法显示