Bläddra i källkod

Merge branch 'develop' into test/StatusList

Nam Nguyen 8 månader sedan
förälder
incheckning
32eec38a5c
57 ändrade filer med 1437 tillägg och 609 borttagningar
  1. 1 1
      frontend/taipy-gui/base/src/app.ts
  2. 434 148
      frontend/taipy-gui/package-lock.json
  3. 2 2
      frontend/taipy-gui/package.json
  4. 17 0
      frontend/taipy-gui/packaging/taipy-gui.d.ts
  5. 7 8
      frontend/taipy-gui/src/components/Taipy/Chat.tsx
  6. 1 1
      frontend/taipy-gui/src/components/Taipy/Expandable.spec.tsx
  7. 51 13
      frontend/taipy-gui/src/components/Taipy/FileSelector.tsx
  8. 1 1
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx
  9. 10 0
      frontend/taipy-gui/src/components/Taipy/Part.spec.tsx
  10. 3 2
      frontend/taipy-gui/src/components/Taipy/Part.tsx
  11. 7 7
      frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
  12. 6 6
      frontend/taipy-gui/src/components/Taipy/TableSort.tsx
  13. 10 0
      frontend/taipy-gui/src/components/Taipy/utils.ts
  14. 2 0
      frontend/taipy-gui/src/extensions/exports.ts
  15. 4 1
      frontend/taipy-gui/src/workers/fileupload.ts
  16. 3 0
      frontend/taipy-gui/src/workers/fileupload.utils.ts
  17. 19 2
      frontend/taipy-gui/src/workers/fileupload.worker.ts
  18. 199 107
      frontend/taipy/package-lock.json
  19. 2 2
      frontend/taipy/package.json
  20. 9 9
      frontend/taipy/src/CoreSelector.tsx
  21. 10 10
      frontend/taipy/src/DataNodeChart.tsx
  22. 6 6
      frontend/taipy/src/DataNodeTable.tsx
  23. 164 79
      frontend/taipy/src/DataNodeViewer.tsx
  24. 15 15
      frontend/taipy/src/JobSelector.tsx
  25. 18 18
      frontend/taipy/src/JobViewer.tsx
  26. 19 30
      frontend/taipy/src/PropertiesEditor.tsx
  27. 17 17
      frontend/taipy/src/ScenarioSelector.tsx
  28. 57 68
      frontend/taipy/src/ScenarioViewer.tsx
  29. 1 1
      frontend/taipy/src/utils/ConfirmDialog.tsx
  30. 23 1
      taipy/core/data/_file_datanode_mixin.py
  31. 1 1
      taipy/core/notification/__init__.py
  32. 2 2
      taipy/core/notification/notifier.py
  33. 2 0
      taipy/core/reason/__init__.py
  34. 28 0
      taipy/core/reason/reason.py
  35. 2 3
      taipy/core/submission/_submission_manager.py
  36. 1 0
      taipy/gui/_default_config.py
  37. 12 1
      taipy/gui/_renderers/_markdown/preproc.py
  38. 2 0
      taipy/gui/_renderers/factory.py
  39. 2 0
      taipy/gui/config.py
  40. 1 2
      taipy/gui/data/utils.py
  41. 32 14
      taipy/gui/gui.py
  42. 24 6
      taipy/gui/server.py
  43. 5 2
      taipy/gui/utils/_evaluator.py
  44. 4 0
      taipy/gui_core/_GuiCoreLib.py
  45. 12 3
      taipy/gui_core/_adapters.py
  46. 47 10
      taipy/gui_core/_context.py
  47. 29 0
      tests/core/data/test_csv_data_node.py
  48. 24 0
      tests/core/data/test_excel_data_node.py
  49. 24 0
      tests/core/data/test_json_data_node.py
  50. 24 0
      tests/core/data/test_parquet_data_node.py
  51. 25 0
      tests/core/data/test_pickle_data_node.py
  52. 2 1
      tests/gui/control/test_date_range.py
  53. 10 0
      tests/gui/control/test_part.py
  54. 1 1
      tests/gui/control/test_text.py
  55. 1 0
      tests/gui/helpers.py
  56. 1 4
      tests/gui_core/test_context_is_editable.py
  57. 1 4
      tests/gui_core/test_context_is_readable.py

+ 1 - 1
frontend/taipy-gui/base/src/app.ts

@@ -243,7 +243,7 @@ export class TaipyApp {
     }
 
     upload(encodedName: string, files: FileList, progressCallback: (val: number) => void) {
-        return uploadFile(encodedName, files, progressCallback, this.clientId);
+        return uploadFile(encodedName, undefined, undefined, undefined, files, progressCallback, this.clientId);
     }
 
     getPageMetadata() {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 434 - 148
frontend/taipy-gui/package-lock.json


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

@@ -5,8 +5,8 @@
   "dependencies": {
     "@emotion/react": "^11.10.0",
     "@emotion/styled": "^11.10.0",
-    "@mui/icons-material": "^5.0.5",
-    "@mui/material": "^5.0.6",
+    "@mui/icons-material": "^6.0.1",
+    "@mui/material": "^6.0.1",
     "@mui/x-date-pickers": "^7.0.0",
     "@mui/x-tree-view": "^7.0.0",
     "apache-arrow": "^14.0.2",

+ 17 - 0
frontend/taipy-gui/packaging/taipy-gui.d.ts

@@ -148,6 +148,23 @@ export interface TableSortProps {
 
 export declare const TableSort: (props: TableSortProps) => JSX.Element;
 
+export interface FileSelectorProps extends TaipyActiveProps {
+    onAction?: string;
+    defaultLabel?: string;
+    label?: string;
+    multiple?: boolean;
+    extensions?: string;
+    dropMessage?: string;
+    notify?: boolean;
+    width?: string | number;
+    icon?: React.ReactNode;
+    withBorder?: boolean;
+    onUploadAction?: string;
+    uploadData?: string;
+}
+
+export declare const FileSelector: (props: FileSelectorProps) => JSX.Element;
+
 export declare const Router: () => JSX.Element;
 
 /**

+ 7 - 8
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -17,7 +17,7 @@ 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 Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputAdornment from "@mui/material/InputAdornment";
 import Paper from "@mui/material/Paper";
@@ -65,7 +65,7 @@ const senderMsgSx = {
 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 nameSx = { fontSize: "0.6em", fontWeight: "bolder", pl: `${indicWidth}em` };
 const senderPaperSx = {
     pr: `${indicWidth}em`,
     pl: `${indicWidth}em`,
@@ -134,22 +134,21 @@ const ChatRow = (props: ChatRowProps) => {
     const avatar = getAvatar(name, sender);
     return (
         <Grid
-            item
             container
             className={getSuffixedClassNames(className, sender ? "-sent" : "-received")}
-            xs={12}
+            size={12}
             sx={noAnchorSx}
             justifyContent={sender ? "flex-end" : undefined}
         >
-            <Grid item sx={sender ? senderMsgSx : undefined}>
+            <Grid sx={sender ? senderMsgSx : undefined}>
                 {avatar ? (
                     <Stack>
                         <Stack direction="row" gap={1}>
-                            <Box sx={avatarColSx}></Box>
+                            <Box sx={avatarColSx}>{avatar}</Box>
                             <Box sx={nameSx}>{name}</Box>
                         </Stack>
                         <Stack direction="row" gap={1}>
-                            <Box sx={avatarColSx}>{avatar}</Box>
+                            <Box sx={avatarColSx}></Box>
                             <Paper sx={sender ? senderPaperSx : otherPaperSx} data-idx={index}>
                                 {message}
                             </Paper>
@@ -352,7 +351,7 @@ const Chat = (props: ChatProps) => {
             <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}>
+                        <Grid className={getSuffixedClassNames(className, "-load")} size={12} sx={noAnchorSx}>
                             <Box sx={loadMoreSx}>
                                 <Button
                                     endIcon={<ArrowUpward />}

+ 1 - 1
frontend/taipy-gui/src/components/Taipy/Expandable.spec.tsx

@@ -33,7 +33,7 @@ describe("Expandable Component", () => {
             </Expandable>
         );
         const elt = getByText("foo");
-        expect(elt.parentElement?.parentElement).toHaveClass("taipy-expandable");
+        expect(elt.parentElement?.parentElement?.parentElement).toHaveClass("taipy-expandable");
     });
     it("displays the default value", async () => {
         const { getByText } = render(

+ 51 - 13
frontend/taipy-gui/src/components/Taipy/FileSelector.tsx

@@ -11,7 +11,17 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
+import React, {
+    ChangeEvent,
+    CSSProperties,
+    ReactNode,
+    useCallback,
+    useContext,
+    useEffect,
+    useMemo,
+    useRef,
+    useState,
+} from "react";
 import Button from "@mui/material/Button";
 import LinearProgress from "@mui/material/LinearProgress";
 import Tooltip from "@mui/material/Tooltip";
@@ -20,8 +30,9 @@ import UploadFile from "@mui/icons-material/UploadFile";
 import { TaipyContext } from "../../context/taipyContext";
 import { createAlertAction, createSendActionNameAction } from "../../context/taipyReducers";
 import { useClassNames, useDynamicProperty, useModule } from "../../utils/hooks";
-import { getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils";
+import { expandSx, getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils";
 import { uploadFile } from "../../workers/fileupload";
+import { SxProps } from "@mui/material";
 
 interface FileSelectorProps extends TaipyActiveProps {
     onAction?: string;
@@ -32,6 +43,10 @@ interface FileSelectorProps extends TaipyActiveProps {
     dropMessage?: string;
     notify?: boolean;
     width?: string | number;
+    icon?: ReactNode;
+    withBorder?: boolean;
+    onUploadAction?: string;
+    uploadData?: string;
 }
 
 const handleDragOver = (evt: DragEvent) => {
@@ -53,9 +68,10 @@ const FileSelector = (props: FileSelectorProps) => {
         dropMessage = "Drop here to Upload",
         label,
         notify = true,
+        withBorder = true,
     } = props;
     const [dropLabel, setDropLabel] = useState("");
-    const [dropSx, setDropSx] = useState(defaultSx);
+    const [dropSx, setDropSx] = useState<SxProps>(defaultSx);
     const [upload, setUpload] = useState(false);
     const [progress, setProgress] = useState(0);
     const { state, dispatch } = useContext(TaipyContext);
@@ -67,7 +83,17 @@ const FileSelector = (props: FileSelectorProps) => {
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
 
-    useEffect(() => setDropSx((sx) => (props.width ? { ...sx, width: getCssSize(props.width) } : sx)), [props.width]);
+    useEffect(
+        () =>
+            setDropSx((sx: SxProps) =>
+                expandSx(
+                    sx,
+                    props.width ? { width: getCssSize(props.width) } : undefined,
+                    withBorder ? undefined : { border: "none" }
+                )
+            ),
+        [props.width, withBorder]
+    );
 
     const handleFiles = useCallback(
         (files: FileList | undefined | null, evt: Event | ChangeEvent) => {
@@ -75,7 +101,15 @@ const FileSelector = (props: FileSelectorProps) => {
             evt.preventDefault();
             if (files?.length) {
                 setUpload(true);
-                uploadFile(updateVarName, files, setProgress, state.id).then(
+                uploadFile(
+                    updateVarName,
+                    module,
+                    props.onUploadAction,
+                    props.uploadData,
+                    files,
+                    setProgress,
+                    state.id
+                ).then(
                     (value) => {
                         setUpload(false);
                         onAction && dispatch(createSendActionNameAction(id, module, onAction));
@@ -94,7 +128,7 @@ const FileSelector = (props: FileSelectorProps) => {
                 );
             }
         },
-        [state.id, id, onAction, notify, updateVarName, dispatch, module]
+        [state.id, id, onAction, props.onUploadAction, props.uploadData, notify, updateVarName, dispatch, module]
     );
 
     const handleChange = useCallback(
@@ -105,7 +139,7 @@ const FileSelector = (props: FileSelectorProps) => {
     const handleDrop = useCallback(
         (e: DragEvent) => {
             setDropLabel("");
-            setDropSx((sx) => ({ ...sx, ...defaultSx }));
+            setDropSx((sx: SxProps) => ({ ...sx, ...defaultSx }));
             handleFiles(e.dataTransfer?.files, e);
         },
         [handleFiles]
@@ -113,15 +147,19 @@ const FileSelector = (props: FileSelectorProps) => {
 
     const handleDragLeave = useCallback(() => {
         setDropLabel("");
-        setDropSx((sx) => ({ ...sx, ...defaultSx }));
+        setDropSx((sx: SxProps) => ({ ...sx, ...defaultSx }));
     }, []);
 
     const handleDragOverWithLabel = useCallback(
         (evt: DragEvent) => {
-            console.log(evt);
             const target = evt.currentTarget as HTMLElement;
-            setDropSx((sx) =>
-                sx.minWidth === defaultSx.minWidth && target ? { ...sx, minWidth: target.clientWidth + "px" } : sx
+            setDropSx((sx: SxProps) =>
+                expandSx(
+                    sx,
+                    (sx as CSSProperties).minWidth === defaultSx.minWidth && target
+                        ? { minWidth: target.clientWidth + "px" }
+                        : undefined
+                )
             );
             setDropLabel(dropMessage);
             handleDragOver(evt);
@@ -164,12 +202,12 @@ const FileSelector = (props: FileSelectorProps) => {
                         id={id}
                         component="span"
                         aria-label="upload"
-                        variant="outlined"
+                        variant={withBorder ? "outlined" : undefined}
                         disabled={!active || upload}
                         sx={dropSx}
                         ref={butRef}
                     >
-                        <UploadFile /> {dropLabel || label || defaultLabel}
+                        {props.icon || <UploadFile />} {dropLabel || label || defaultLabel}
                     </Button>
                 </span>
             </Tooltip>

+ 1 - 1
frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx

@@ -727,7 +727,7 @@ describe("PaginatedTable Component", () => {
         const option = queryByRole("option", { selected: false, name: "50" });
         fireEvent.click(option as Element);
         const table = document.querySelector(
-            'table[aria-labelledby="tableTitle"].MuiTable-root.MuiTable-stickyHeader.css-cz602z-MuiTable-root'
+            'table[aria-labelledby="tableTitle"].MuiTable-root.MuiTable-stickyHeader'
         );
         expect(table).toBeInTheDocument();
     });

+ 10 - 0
frontend/taipy-gui/src/components/Taipy/Part.spec.tsx

@@ -29,6 +29,16 @@ describe("Part Component", () => {
         const elt = getByText("bar");
         expect(elt).toHaveClass("taipy-part");
     })
+    it("displays with width=70%", async () => {
+        const { getByText } = render(<Part width="70%">bar</Part>);
+        const elt = getByText("bar");
+        expect(elt).toHaveStyle('width: 70%');
+    });
+    it("displays with width=500", async () => {
+        const { getByText } = render(<Part width={500}>bar</Part>);
+        const elt = getByText("bar");
+        expect(elt).toHaveStyle('width: 500px');
+    });
     it("renders an iframe", async () => {
         const {getByText} = render(<Part className="taipy-part" page="http://taipy.io">bar</Part>);
         const elt = getByText("bar");

+ 3 - 2
frontend/taipy-gui/src/components/Taipy/Part.tsx

@@ -16,7 +16,7 @@ import Box from "@mui/material/Box";
 
 import { useClassNames, useDynamicProperty } from "../../utils/hooks";
 import TaipyRendered from "../pages/TaipyRendered";
-import { TaipyBaseProps } from "./utils";
+import { expandSx, getCssSize, TaipyBaseProps } from "./utils";
 import { TaipyContext } from "../../context/taipyContext";
 
 interface PartProps extends TaipyBaseProps {
@@ -29,6 +29,7 @@ interface PartProps extends TaipyBaseProps {
     partial?: boolean;
     height?: string;
     defaultHeight?: string;
+    width?: string | number;
 }
 
 const IframeStyle = {
@@ -55,7 +56,7 @@ const Part = (props: PartProps) => {
         return false;
     }, [state.locations, page, defaultPartial]);
 
-    const boxSx = useMemo(() => (height ? { height: height } : undefined), [height]);
+    const boxSx = useMemo(() => expandSx({}, height ? { height: height } : undefined, props.width ? {width: getCssSize(props.width)}: undefined), [height, props.width]);
     return render ? (
         <Box id={id} className={className} sx={boxSx}>
             {iFrame ? (

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

@@ -18,7 +18,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
 import FilterListIcon from "@mui/icons-material/FilterList";
 import Badge from "@mui/material/Badge";
 import FormControl from "@mui/material/FormControl";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import MenuItem from "@mui/material/MenuItem";
@@ -208,8 +208,8 @@ const FilterRow = (props: FilterRowProps) => {
     const colLov = colId in columns && columns[colId].lov ? columns[colId].lov : undefined;
 
     return (
-        <Grid container item xs={12} alignItems="center">
-            <Grid item xs={3.5}>
+        <Grid container size={12} alignItems="center">
+            <Grid size={3.5}>
                 <FormControl margin="dense">
                     <InputLabel>Column</InputLabel>
                     <Select value={colId || ""} onChange={onColSelect} input={<OutlinedInput label="Column" />}>
@@ -223,7 +223,7 @@ const FilterRow = (props: FilterRowProps) => {
                     </Select>
                 </FormControl>
             </Grid>
-            <Grid item xs={3}>
+            <Grid size={3}>
                 <FormControl margin="dense">
                     <InputLabel>Action</InputLabel>
                     <Select value={action || ""} onChange={onActSelect} input={<OutlinedInput label="Action" />}>
@@ -235,7 +235,7 @@ const FilterRow = (props: FilterRowProps) => {
                     </Select>
                 </FormControl>
             </Grid>
-            <Grid item xs={3.5}>
+            <Grid size={3.5}>
                 {colType == "number" ? (
                     <TextField
                         type="number"
@@ -290,7 +290,7 @@ const FilterRow = (props: FilterRowProps) => {
                     />
                 )}
             </Grid>
-            <Grid item xs={1}>
+            <Grid size={1}>
                 <Tooltip title="Validate">
                     <span>
                         <IconButton onClick={onCheckClick} disabled={!enableCheck} sx={iconInRowSx}>
@@ -299,7 +299,7 @@ const FilterRow = (props: FilterRowProps) => {
                     </span>
                 </Tooltip>
             </Grid>
-            <Grid item xs={1}>
+            <Grid size={1}>
                 <Tooltip title="Delete">
                     <span>
                         <IconButton onClick={onDeleteClick} disabled={!enableDel} sx={iconInRowSx}>

+ 6 - 6
frontend/taipy-gui/src/components/Taipy/TableSort.tsx

@@ -17,7 +17,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
 import SortByAlpha from "@mui/icons-material/SortByAlpha";
 import Badge from "@mui/material/Badge";
 import FormControl from "@mui/material/FormControl";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import MenuItem from "@mui/material/MenuItem";
@@ -129,8 +129,8 @@ const SortRow = (props: SortRowProps) => {
     }, [columns, sort, idx]);
 
     return cols.length ? (
-        <Grid container item xs={12} alignItems="center">
-            <Grid item xs={6}>
+        <Grid container size={12} alignItems="center">
+            <Grid size={6}>
                 <FormControl margin="dense">
                     <InputLabel>Column</InputLabel>
                     <Select value={colId || ""} onChange={onColSelect} input={<OutlinedInput label="Column" />}>
@@ -142,13 +142,13 @@ const SortRow = (props: SortRowProps) => {
                     </Select>
                 </FormControl>
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Switch checked={order} onChange={onOrderSwitch} />
                 <Typography variant="caption" color="text.secondary" sx={orderCaptionSx}>
                     {order ? "asc" : "desc"}
                 </Typography>
             </Grid>
-            <Grid item xs={1}>
+            <Grid size={1}>
                 <Tooltip title="Validate">
                     <span>
                         <IconButton onClick={onCheckClick} disabled={!enableCheck} sx={iconInRowSx}>
@@ -157,7 +157,7 @@ const SortRow = (props: SortRowProps) => {
                     </span>
                 </Tooltip>
             </Grid>
-            <Grid item xs={1}>
+            <Grid size={1}>
                 <Tooltip title="Delete">
                     <span>
                         <IconButton onClick={onDeleteClick} disabled={!enableDel} sx={iconInRowSx}>

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

@@ -12,6 +12,7 @@
  */
 
 import { MouseEvent } from "react";
+import { SxProps } from "@mui/material";
 
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
     defaultActive?: boolean;
@@ -146,3 +147,12 @@ export const getProps = (p: DateProps, start: boolean, val: Date | null, withTim
     }
     return { ...p, [propName]: val };
 };
+
+export const expandSx = (sx: SxProps, ...partials: (SxProps | undefined)[]) => {
+    return partials.reduce((prevSx: SxProps, partialSx) => {
+        if (partialSx) {
+            return { ...prevSx, ...partialSx } as SxProps;
+        }
+        return prevSx;
+    }, sx);
+};

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

@@ -13,6 +13,7 @@
 
 import Chart from "../components/Taipy/Chart";
 import Dialog from "../components/Taipy/Dialog";
+import FileSelector from "../components/Taipy/FileSelector";
 import Login from "../components/Taipy/Login";
 import Router from "../components/Router";
 import Table from "../components/Taipy/Table";
@@ -43,6 +44,7 @@ import {
 export {
     Chart,
     Dialog,
+    FileSelector,
     Login,
     Router,
     Table,

+ 4 - 1
frontend/taipy-gui/src/workers/fileupload.ts

@@ -19,6 +19,9 @@ const UPLOAD_URL = "/taipy-uploads";
 
 export const uploadFile = (
     varName: string,
+    context: string | undefined,
+    onAction: string | undefined,
+    uploadData: string | undefined,
     files: FileList,
     progressCallback: (val: number) => void,
     id: string,
@@ -35,6 +38,6 @@ export const uploadFile = (
             }
         };
         worker.onerror = (evt: ErrorEvent) => reject(evt);
-        worker.postMessage({ files: files, uploadUrl: uploadUrl, varName: varName, id: id } as FileUploadData);
+        worker.postMessage({ files, uploadUrl, varName, context, onAction, uploadData, id } as FileUploadData);
     });
 };

+ 3 - 0
frontend/taipy-gui/src/workers/fileupload.utils.ts

@@ -13,6 +13,9 @@
 
 export interface FileUploadData {
     varName: string;
+    context?: string;
+    onAction?: string;
+    uploadData?: string;
     files: FileList;
     uploadUrl: string;
     id: string;

+ 19 - 2
frontend/taipy-gui/src/workers/fileupload.worker.ts

@@ -17,6 +17,9 @@ const uploadFile = (
     blobOrFile: Blob,
     uploadUrl: string,
     varName: string,
+    context: string | undefined,
+    onAction: string | undefined,
+    uploadData: string | undefined,
     part: number,
     total: number,
     fileName: string,
@@ -33,6 +36,9 @@ const uploadFile = (
     fdata.append("part", part.toString());
     fdata.append("total", total.toString());
     fdata.append("var_name", varName);
+    context && fdata.append("context", context);
+    onAction && fdata.append("on_action", onAction);
+    uploadData && fdata.append("upload_data", uploadData);
     fdata.append("multiple", multiple ? "True" : "False");
     xhr.send(fdata);
 };
@@ -46,7 +52,15 @@ const getProgressCallback = (globalSize: number, offset: number) => (uploaded: n
         done: false,
     } as FileUploadReturn);
 
-const process = (files: FileList, uploadUrl: string, varName: string, id: string) => {
+const process = (
+    files: FileList,
+    uploadUrl: string,
+    varName: string,
+    context: string | undefined,
+    onAction: string | undefined,
+    uploadData: string | undefined,
+    id: string
+) => {
     if (files) {
         let globalSize = 0;
         for (let i = 0; i < files.length; i++) {
@@ -70,6 +84,9 @@ const process = (files: FileList, uploadUrl: string, varName: string, id: string
                     chunk,
                     uploadUrl,
                     varName,
+                    context,
+                    onAction,
+                    uploadData,
                     Math.floor(start / BYTES_PER_CHUNK),
                     tot,
                     blob.name,
@@ -94,5 +111,5 @@ const process = (files: FileList, uploadUrl: string, varName: string, id: string
 };
 
 self.onmessage = (e: MessageEvent<FileUploadData>) => {
-    process(e.data.files, e.data.uploadUrl, e.data.varName, e.data.id);
+    process(e.data.files, e.data.uploadUrl, e.data.varName, e.data.context, e.data.onAction, e.data.uploadData, e.data.id);
 };

+ 199 - 107
frontend/taipy/package-lock.json

@@ -11,8 +11,8 @@
       "dependencies": {
         "@emotion/react": "^11.10.6",
         "@emotion/styled": "^11.10.8",
-        "@mui/icons-material": "^5.0.5",
-        "@mui/material": "^5.0.6",
+        "@mui/icons-material": "^6.0.1",
+        "@mui/material": "^6.0.1",
         "@mui/x-date-pickers": "^7.0.0",
         "@mui/x-tree-view": "^7.0.0",
         "@projectstorm/react-diagrams": "^7.0.2",
@@ -41,6 +41,7 @@
       }
     },
     "../../taipy/gui/webapp": {
+      "name": "taipy-gui",
       "version": "4.0.0"
     },
     "node_modules/@babel/code-frame": {
@@ -56,11 +57,11 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.25.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
-      "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
+      "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
       "dependencies": {
-        "@babel/types": "^7.25.4",
+        "@babel/types": "^7.25.6",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -112,11 +113,11 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz",
-      "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==",
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
+      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
       "dependencies": {
-        "@babel/types": "^7.25.4"
+        "@babel/types": "^7.25.6"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -126,9 +127,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz",
-      "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==",
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
+      "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -150,15 +151,15 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz",
-      "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==",
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz",
+      "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.25.4",
-        "@babel/parser": "^7.25.4",
+        "@babel/generator": "^7.25.6",
+        "@babel/parser": "^7.25.6",
         "@babel/template": "^7.25.0",
-        "@babel/types": "^7.25.4",
+        "@babel/types": "^7.25.6",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -167,9 +168,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
-      "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
+      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
       "dependencies": {
         "@babel/helper-string-parser": "^7.24.8",
         "@babel/helper-validator-identifier": "^7.24.7",
@@ -664,32 +665,32 @@
       "dev": true
     },
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.16.7",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz",
-      "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.0.1.tgz",
+      "integrity": "sha512-TmKkCTwgtwvlFTF1tZzG4lYbi7v6NGweEJwFBZoIWZrkF1OLa0xu4umifmIyd+bVIScsEj//E2AD6bOJbPMOOQ==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
       }
     },
     "node_modules/@mui/icons-material": {
-      "version": "5.16.7",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz",
-      "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.0.1.tgz",
+      "integrity": "sha512-CsgaF65jA3H1YzpDg6H2nFH/UHueVlmpEtPim7xF9VbjYnmnblG3aX0GflBahH96Pg0schrFWyRySlgbVAh5Kw==",
       "dependencies": {
-        "@babel/runtime": "^7.23.9"
+        "@babel/runtime": "^7.25.0"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
       },
       "peerDependencies": {
-        "@mui/material": "^5.0.0",
-        "@types/react": "^17.0.0 || ^18.0.0",
-        "react": "^17.0.0 || ^18.0.0"
+        "@mui/material": "^6.0.1",
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -698,25 +699,25 @@
       }
     },
     "node_modules/@mui/material": {
-      "version": "5.16.7",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz",
-      "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.0.1.tgz",
+      "integrity": "sha512-gOJS0RKYs9lRACaTluXPNopxFpIBhWVmhf09lHpqpPlR6bujXhuiTE2Q8puensdz3Qm2JGzl1VjccYHieV1g8A==",
       "dependencies": {
-        "@babel/runtime": "^7.23.9",
-        "@mui/core-downloads-tracker": "^5.16.7",
-        "@mui/system": "^5.16.7",
-        "@mui/types": "^7.2.15",
-        "@mui/utils": "^5.16.6",
+        "@babel/runtime": "^7.25.0",
+        "@mui/core-downloads-tracker": "^6.0.1",
+        "@mui/system": "^6.0.1",
+        "@mui/types": "^7.2.16",
+        "@mui/utils": "^6.0.1",
         "@popperjs/core": "^2.11.8",
-        "@types/react-transition-group": "^4.4.10",
-        "clsx": "^2.1.0",
+        "@types/react-transition-group": "^4.4.11",
+        "clsx": "^2.1.1",
         "csstype": "^3.1.3",
         "prop-types": "^15.8.1",
         "react-is": "^18.3.1",
         "react-transition-group": "^4.4.5"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -725,9 +726,10 @@
       "peerDependencies": {
         "@emotion/react": "^11.5.0",
         "@emotion/styled": "^11.3.0",
-        "@types/react": "^17.0.0 || ^18.0.0",
-        "react": "^17.0.0 || ^18.0.0",
-        "react-dom": "^17.0.0 || ^18.0.0"
+        "@mui/material-pigment-css": "^6.0.1",
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@emotion/react": {
@@ -736,30 +738,91 @@
         "@emotion/styled": {
           "optional": true
         },
+        "@mui/material-pigment-css": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/material/node_modules/@mui/utils": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.1.tgz",
+      "integrity": "sha512-YmQYb2tY5nJactHltTrKA15TZfbd1R003a2xYHxUuycTv9n83rsIwHkypOxM4x7+c+Pc8xfCuE9EfLT3B3n40Q==",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "@mui/types": "^7.2.16",
+        "@types/prop-types": "^15.7.12",
+        "clsx": "^2.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.3.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
         "@types/react": {
           "optional": true
         }
       }
     },
     "node_modules/@mui/private-theming": {
-      "version": "5.16.6",
-      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz",
-      "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.0.1.tgz",
+      "integrity": "sha512-jQCJml1OwIrhqN5tTk5Lpqx2RZKQnShE8lMlvAkuO7Ft+xaHkP8J3iHpEk3/Pzue34DfBQtK00jcaplgM47mBA==",
       "dependencies": {
-        "@babel/runtime": "^7.23.9",
-        "@mui/utils": "^5.16.6",
+        "@babel/runtime": "^7.25.0",
+        "@mui/utils": "^6.0.1",
         "prop-types": "^15.8.1"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
       },
       "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0",
-        "react": "^17.0.0 || ^18.0.0"
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/private-theming/node_modules/@mui/utils": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.1.tgz",
+      "integrity": "sha512-YmQYb2tY5nJactHltTrKA15TZfbd1R003a2xYHxUuycTv9n83rsIwHkypOxM4x7+c+Pc8xfCuE9EfLT3B3n40Q==",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "@mui/types": "^7.2.16",
+        "@types/prop-types": "^15.7.12",
+        "clsx": "^2.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.3.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -768,17 +831,17 @@
       }
     },
     "node_modules/@mui/styled-engine": {
-      "version": "5.16.6",
-      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz",
-      "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.0.1.tgz",
+      "integrity": "sha512-7ZOnUhIak2vosDgMlBE/oLrsvvF3O8QKmTFpP6bhZkHjPu4dv0DbF1vC7gzgkOqiMaT0/NgRQCFW9zh38pIvsg==",
       "dependencies": {
-        "@babel/runtime": "^7.23.9",
-        "@emotion/cache": "^11.11.0",
+        "@babel/runtime": "^7.25.0",
+        "@emotion/cache": "^11.13.1",
         "csstype": "^3.1.3",
         "prop-types": "^15.8.1"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -787,7 +850,7 @@
       "peerDependencies": {
         "@emotion/react": "^11.4.1",
         "@emotion/styled": "^11.3.0",
-        "react": "^17.0.0 || ^18.0.0"
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@emotion/react": {
@@ -799,21 +862,21 @@
       }
     },
     "node_modules/@mui/system": {
-      "version": "5.16.7",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz",
-      "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.0.1.tgz",
+      "integrity": "sha512-RdWyCMi+GkAekOnpMKhy51lyzid4F6Vj96vekp3AExkFY21JWg2+KVBqcAgJOROJ3RiaeDJf98n0yrixlCvuEw==",
       "dependencies": {
-        "@babel/runtime": "^7.23.9",
-        "@mui/private-theming": "^5.16.6",
-        "@mui/styled-engine": "^5.16.6",
-        "@mui/types": "^7.2.15",
-        "@mui/utils": "^5.16.6",
-        "clsx": "^2.1.0",
+        "@babel/runtime": "^7.25.0",
+        "@mui/private-theming": "^6.0.1",
+        "@mui/styled-engine": "^6.0.1",
+        "@mui/types": "^7.2.16",
+        "@mui/utils": "^6.0.1",
+        "clsx": "^2.1.1",
         "csstype": "^3.1.3",
         "prop-types": "^15.8.1"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=14.0.0"
       },
       "funding": {
         "type": "opencollective",
@@ -822,8 +885,8 @@
       "peerDependencies": {
         "@emotion/react": "^11.5.0",
         "@emotion/styled": "^11.3.0",
-        "@types/react": "^17.0.0 || ^18.0.0",
-        "react": "^17.0.0 || ^18.0.0"
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@emotion/react": {
@@ -837,12 +900,41 @@
         }
       }
     },
+    "node_modules/@mui/system/node_modules/@mui/utils": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.1.tgz",
+      "integrity": "sha512-YmQYb2tY5nJactHltTrKA15TZfbd1R003a2xYHxUuycTv9n83rsIwHkypOxM4x7+c+Pc8xfCuE9EfLT3B3n40Q==",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "@mui/types": "^7.2.16",
+        "@types/prop-types": "^15.7.12",
+        "clsx": "^2.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^18.3.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@mui/types": {
-      "version": "7.2.15",
-      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz",
-      "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==",
+      "version": "7.2.16",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz",
+      "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==",
       "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0"
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@types/react": {
@@ -880,12 +972,11 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.14.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.14.0.tgz",
-      "integrity": "sha512-3xI3xYVxqPU4//KfE4FcR+Zs7UT4kkDPvA+IDOcQdRUyVwmcXCjBuJZgKgJMqSCNK/KIJZQQrpmy5XGHOKTbdA==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz",
+      "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==",
       "dependencies": {
-        "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.7",
+        "@babel/runtime": "^7.25.4",
         "@mui/utils": "^5.16.6",
         "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
@@ -902,7 +993,8 @@
       "peerDependencies": {
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",
-        "@mui/material": "^5.15.14",
+        "@mui/material": "^5.15.14 || ^6.0.0",
+        "@mui/system": "^5.15.14 || ^6.0.0",
         "date-fns": "^2.25.0 || ^3.2.0",
         "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
         "dayjs": "^1.10.7",
@@ -944,11 +1036,11 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.14.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.14.0.tgz",
-      "integrity": "sha512-+qWIHLgt2vgH6bKmf7IwRvS86UbZRWKAdDY/yTQJaqzCzyesUvQhD+WRxe1kpdCK8UE061S9/Ju7hLkM4kjRNA==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.15.0.tgz",
+      "integrity": "sha512-Q/IZvZhHpe64Ost1mRbdp6ML8KQQBprwwgzqo6pZbrCaWMPB2gk2jcUwdCwnLsc+gutaEPVhZ8N7it8VZcHtbg==",
       "dependencies": {
-        "@babel/runtime": "^7.25.0",
+        "@babel/runtime": "^7.25.4",
         "@mui/utils": "^5.16.6"
       },
       "engines": {
@@ -963,14 +1055,13 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.14.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.14.0.tgz",
-      "integrity": "sha512-j1sK0tLrsiCu0FxwTJQkVm2nbLYc1tRLwmPDAXcQ3nuzGDzn0x/IA28dBjxse/+oNy4j2cpJz3k/mSz/a4ZLjA==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.15.0.tgz",
+      "integrity": "sha512-iMhI+ktZWnWMB60uCToGwTIGpmC17LT6o+fV1QSLgh8gWHjTKxJHHVIJnwX4GGeg+CDZQrodkAcGwu54txlStQ==",
       "dependencies": {
-        "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.7",
+        "@babel/runtime": "^7.25.4",
         "@mui/utils": "^5.16.6",
-        "@mui/x-internals": "7.14.0",
+        "@mui/x-internals": "7.15.0",
         "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
@@ -986,7 +1077,8 @@
       "peerDependencies": {
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",
-        "@mui/material": "^5.15.14",
+        "@mui/material": "^5.15.14 || ^6.0.0",
+        "@mui/system": "^5.15.14 || ^6.0.0",
         "react": "^17.0.0 || ^18.0.0",
         "react-dom": "^17.0.0 || ^18.0.0"
       },
@@ -1179,9 +1271,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "22.5.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
-      "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
+      "version": "22.5.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz",
+      "integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==",
       "dev": true,
       "dependencies": {
         "undici-types": "~6.19.2"
@@ -1198,9 +1290,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     "node_modules/@types/react": {
-      "version": "18.3.4",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
-      "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
+      "version": "18.3.5",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
+      "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
       "dependencies": {
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
@@ -2003,9 +2095,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001653",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
-      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
+      "version": "1.0.30001655",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz",
+      "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==",
       "dev": true,
       "funding": [
         {
@@ -2547,9 +2639,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
       "engines": {
         "node": ">=6"

+ 2 - 2
frontend/taipy/package.json

@@ -21,8 +21,8 @@
   "dependencies": {
     "@emotion/react": "^11.10.6",
     "@emotion/styled": "^11.10.8",
-    "@mui/icons-material": "^5.0.5",
-    "@mui/material": "^5.0.6",
+    "@mui/icons-material": "^6.0.1",
+    "@mui/material": "^6.0.1",
     "@mui/x-date-pickers": "^7.0.0",
     "@mui/x-tree-view": "^7.0.0",
     "@projectstorm/react-diagrams": "^7.0.2",

+ 9 - 9
frontend/taipy/src/CoreSelector.tsx

@@ -24,7 +24,7 @@ import React, {
 import { TextField, Theme, alpha } from "@mui/material";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
 import FormControlLabel from "@mui/material/FormControlLabel";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import Switch from "@mui/material/Switch";
 import Tooltip from "@mui/material/Tooltip";
@@ -167,7 +167,7 @@ const CoreItem = (props: {
             data-selectable={nodeType === props.leafType}
             label={
                 <Grid container alignItems="center" direction="row" flexWrap="nowrap" spacing={1}>
-                    <Grid item xs sx={iconLabelSx}>
+                    <Grid  size="grow" sx={iconLabelSx}>
                         {nodeType === NodeType.CYCLE ? (
                             <CycleIcon fontSize="small" color="primary" />
                         ) : nodeType === NodeType.SCENARIO ? (
@@ -191,12 +191,12 @@ const CoreItem = (props: {
                         {label}
                     </Grid>
                     {props.editComponent && nodeType === props.leafType ? (
-                        <Grid item xs="auto">
+                        <Grid size="auto">
                             <props.editComponent id={id} active={props.active} />
                         </Grid>
                     ) : null}
                     {props.onPin ? (
-                        <Grid item xs="auto">
+                        <Grid size="auto">
                             <Tooltip title={isPinned ? "Unpin" : "Pin"}>
                                 <IconButton
                                     data-id={id}
@@ -631,7 +631,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
         <>
             <Grid container sx={switchBoxSx} gap={1}>
                 {active && colFilters ? (
-                    <Grid item>
+                    <Grid>
                         <TableFilter
                             columns={colFilters}
                             appliedFilters={filters}
@@ -641,12 +641,12 @@ const CoreSelector = (props: CoreSelectorProps) => {
                     </Grid>
                 ) : null}
                 {active && colSorts ? (
-                    <Grid item>
+                    <Grid>
                         <TableSort columns={colSorts} appliedSorts={sorts} onValidate={applySorts}></TableSort>
                     </Grid>
                 ) : null}
                 {showSearch ? (
-                    <Grid item>
+                    <Grid>
                         <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
                             {revealSearch ? (
                                 <SearchOffOutlined fontSize="inherit" />
@@ -657,7 +657,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                     </Grid>
                 ) : null}
                 {showPins ? (
-                    <Grid item>
+                    <Grid>
                         <FormControlLabel
                             control={
                                 <Switch
@@ -673,7 +673,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                     </Grid>
                 ) : null}
                 {showSearch && revealSearch ? (
-                    <Grid item xs={12}>
+                    <Grid size={12}>
                         <TextField
                             margin="dense"
                             value={searchValue}

+ 10 - 10
frontend/taipy/src/DataNodeChart.tsx

@@ -23,7 +23,7 @@ import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import FormControl from "@mui/material/FormControl";
 import FormControlLabel from "@mui/material/FormControlLabel";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import OutlinedInput from "@mui/material/OutlinedInput";
@@ -370,7 +370,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
     return (
         <>
             <Grid container sx={tabularHeaderSx}>
-                <Grid item>
+                <Grid>
                     <Box className="taipy-toggle">
                         <ToggleButtonGroup onChange={onViewTypeChange} exclusive value={ChartViewType} color="primary">
                             <ToggleButton value={TableViewType}>
@@ -382,7 +382,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                         </ToggleButtonGroup>
                     </Box>
                 </Grid>
-                <Grid item>
+                <Grid>
                     <FormControlLabel
                         control={
                             <Switch checked={!!config?.cumulative} onChange={onCumulativeChange} color="primary" />
@@ -390,7 +390,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                         label="Cumulative"
                     />
                 </Grid>
-                <Grid item>
+                <Grid>
                     <Button
                         onClick={resetConfig}
                         variant="text"
@@ -409,10 +409,10 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                               const baseLabelId = `${uniqid}-trace${idx}-"`;
                               return (
                                   <Fragment key={idx}>
-                                      <Grid item xs={2} sx={TraceSx}>
+                                      <Grid size={2} sx={TraceSx}>
                                           Trace {idx + 1}
                                       </Grid>
-                                      <Grid item xs={3}>
+                                      <Grid size={3}>
                                           <TypeSelect
                                               trace={idx}
                                               label="Category"
@@ -421,7 +421,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                                               value={config.types ? config.types[idx] : ""}
                                           />
                                       </Grid>
-                                      <Grid item xs={3}>
+                                      <Grid size={3}>
                                           <ColSelect
                                               trace={idx}
                                               axis={0}
@@ -432,7 +432,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                                               setColConf={setColConf}
                                           />{" "}
                                       </Grid>
-                                      <Grid item xs={3}>
+                                      <Grid size={3}>
                                           <ColSelect
                                               trace={idx}
                                               axis={1}
@@ -444,7 +444,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                                               withNone
                                           />
                                       </Grid>
-                                      <Grid item xs={1}>
+                                      <Grid size={1}>
                                           {config.traces && config.traces.length > 1 ? (
                                               <Tooltip title="Remove Trace">
                                                   <IconButton onClick={onRemoveTrace} data-idx={idx}>
@@ -457,7 +457,7 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                               );
                           })
                         : null}
-                    <Grid item xs={12}>
+                    <Grid size={12}>
                         <Button onClick={onAddTrace} startIcon={<Add color="primary" />}>
                             Add trace
                         </Button>

+ 6 - 6
frontend/taipy/src/DataNodeTable.tsx

@@ -22,7 +22,7 @@ import Button from "@mui/material/Button";
 import Checkbox from "@mui/material/Checkbox";
 import FormControl from "@mui/material/FormControl";
 import FormControlLabel from "@mui/material/FormControlLabel";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import InputLabel from "@mui/material/InputLabel";
 import OutlinedInput from "@mui/material/OutlinedInput";
 import ListItemText from "@mui/material/ListItemText";
@@ -150,7 +150,7 @@ const DataNodeTable = (props: DataNodeTableProps) => {
     return (
         <>
             <Grid container sx={tabularHeaderSx}>
-                <Grid item>
+                <Grid>
                     <Box className="taipy-toggle">
                         <ToggleButtonGroup onChange={onViewTypeChange} exclusive value={TableViewType} color="primary">
                             <ToggleButton value={TableViewType}>
@@ -162,7 +162,7 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                         </ToggleButtonGroup>
                     </Box>
                 </Grid>
-                <Grid item>
+                <Grid>
                     <FormControl sx={selectSx} fullWidth className="taipy-selector">
                         <InputLabel id={uniqid + "-cols-label"}>Columns</InputLabel>
                         <Select
@@ -184,7 +184,7 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                         </Select>
                     </FormControl>
                 </Grid>
-                <Grid item>
+                <Grid>
                     <Button
                         onClick={resetCols}
                         variant="text"
@@ -196,11 +196,11 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                     </Button>
                 </Grid>
                 {tableEdit ? (
-                    <Grid item sx={pushRightSx}>
+                    <Grid sx={pushRightSx}>
                         <TextField value={comment} onChange={changeComment} label="Comment"></TextField>
                     </Grid>
                 ) : null}
-                <Grid item sx={tableEdit ? undefined : pushRightSx}>
+                <Grid sx={tableEdit ? undefined : pushRightSx}>
                     <FormControlLabel
                         disabled={!props.active || !!notEditableReason || !!props.editInProgress}
                         control={<Switch color="primary" checked={tableEdit} onChange={toggleTableEdit} />}

+ 164 - 79
frontend/taipy/src/DataNodeViewer.tsx

@@ -28,23 +28,27 @@ import AccordionDetails from "@mui/material/AccordionDetails";
 import AccordionSummary from "@mui/material/AccordionSummary";
 import Alert from "@mui/material/Alert";
 import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
 import Divider from "@mui/material/Divider";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputAdornment from "@mui/material/InputAdornment";
 import Popover from "@mui/material/Popover";
 import Switch from "@mui/material/Switch";
+import Stack from "@mui/material/Stack";
 import Tab from "@mui/material/Tab";
 import Tabs from "@mui/material/Tabs";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
 
-import CheckCircle from "@mui/icons-material/CheckCircle";
-import Cancel from "@mui/icons-material/Cancel";
 import ArrowForwardIosSharp from "@mui/icons-material/ArrowForwardIosSharp";
+import Cancel from "@mui/icons-material/Cancel";
+import CheckCircle from "@mui/icons-material/CheckCircle";
+import Download from "@mui/icons-material/Download";
 import Launch from "@mui/icons-material/Launch";
 import LockOutlined from "@mui/icons-material/LockOutlined";
+import Upload from "@mui/icons-material/Upload";
 
 import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
 import { BaseDateTimePickerSlotProps } from "@mui/x-date-pickers/DateTimePicker/shared";
@@ -64,6 +68,7 @@ import {
     useDynamicProperty,
     useModule,
     Store,
+    FileSelector,
 } from "taipy-gui";
 
 import { Cycle as CycleIcon, Scenario as ScenarioIcon } from "./icons";
@@ -100,6 +105,7 @@ const editSx = {
     "& > div": { writingMode: "vertical-rl", transform: "rotate(180deg)", paddingBottom: "1em" },
 };
 const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotProps<Date>;
+const buttonSx = { minWidth: "0px" };
 
 type DataNodeFull = [
     string, // id
@@ -115,7 +121,10 @@ type DataNodeFull = [
     boolean, // editInProgress
     string, // editorId
     string, // notReadableReason
-    string // notEditableReason
+    string, // notEditableReason
+    boolean, // is file based
+    string, // notDownloadableReason
+    string // notUploadableReason
 ];
 
 enum DataNodeFullProps {
@@ -133,6 +142,9 @@ enum DataNodeFullProps {
     editorId,
     notReadableReason,
     notEditableReason,
+    isFileBased,
+    notDownloadableReason,
+    notUploadableReason,
 }
 const DataNodeFullLength = Object.keys(DataNodeFullProps).length / 2;
 
@@ -180,6 +192,10 @@ interface DataNodeViewerProps {
     width?: string;
     onLock?: string;
     updateDnVars?: string;
+    fileDownload?: boolean;
+    fileUpload?: boolean;
+    uploadCheck?: string;
+    onFileAction?: string;
 }
 
 const dataValueFocus = "data-value";
@@ -208,6 +224,9 @@ const invalidDatanode: DataNodeFull = [
     "",
     "invalid",
     "invalid",
+    false,
+    "invalid",
+    "invalid",
 ];
 
 enum TabValues {
@@ -230,6 +249,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         showData = true,
         updateVars = "",
         updateDnVars = "",
+        fileDownload = false,
+        fileUpload = false,
     } = props;
 
     const { state, dispatch } = useContext<Store>(Context);
@@ -255,6 +276,9 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         dnEditorId,
         dnNotReadableReason,
         dnNotEditableReason,
+        isFileBased,
+        dnNotDownloadableReason,
+        dnNotUploadableReason,
     ] = datanode;
     const dtType = dnData[DatanodeDataProps.type];
     const dtValue = dnData[DatanodeDataProps.value] ?? (dtType == "float" ? null : undefined);
@@ -611,6 +635,37 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         [props.width]
     );
 
+    // file action
+    const onfileHandler = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            const { action = "import" } = e.currentTarget.dataset || {};
+            if (action == "export") {
+                dispatch(
+                    createSendActionNameAction(id, module, props.onFileAction, {
+                        id: dnId,
+                        action: action,
+                        type: "raw",
+                        error_id: getUpdateVar(updateDnVars, "error_id"),
+                    })
+                );
+            }
+        },
+        [props.onFileAction, dispatch, dnId, id, module, updateDnVars]
+    );
+
+    const uploadData = useMemo(
+        () =>
+            valid && isFileBased && fileUpload
+                ? JSON.stringify({
+                      id: dnId,
+                      error_id: getUpdateVar(updateDnVars, "error_id"),
+                      upload_check: props.uploadCheck,
+                  })
+                : undefined,
+        [dnId, valid, isFileBased, fileUpload, props.uploadCheck, updateDnVars]
+    );
+
     // Refresh on broadcast
     useEffect(() => {
         const ids = props.coreChanged?.datanode;
@@ -627,50 +682,82 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                         expandIcon={expandable ? <ArrowForwardIosSharp sx={AccordionIconSx} /> : null}
                         sx={AccordionSummarySx}
                     >
-                        <Grid container alignItems="baseline" direction="row" spacing={1}>
-                            <Grid item>{dnLabel}</Grid>
-                            <Grid item>
-                                <Typography fontSize="smaller">{dnType}</Typography>
-                            </Grid>
-                        </Grid>
+                        <Stack direction="row" spacing={1} alignItems="center">
+                            <Typography>{dnLabel}</Typography>
+                            <Typography fontSize="smaller">{dnType}</Typography>
+                        </Stack>
                     </AccordionSummary>
                     <AccordionDetails>
                         <Box sx={tabBoxSx}>
-                            <Tabs value={tabValue} onChange={handleTabChange}>
-                                <Tab
-                                    label={
-                                        <Grid container alignItems="center">
-                                            <Grid item>Data</Grid>
-                                            {dnEditInProgress ? (
-                                                <Grid item>
-                                                    <Tooltip
-                                                        title={"locked " + (dnEditorId === editorId ? "by you" : "")}
+                            <Stack direction="row" justifyContent="space-between">
+                                <Tabs value={tabValue} onChange={handleTabChange}>
+                                    <Tab
+                                        label={
+                                            <Grid container alignItems="center">
+                                                <Grid>Data</Grid>
+                                                {dnEditInProgress ? (
+                                                    <Grid>
+                                                        <Tooltip
+                                                            title={
+                                                                "locked " + (dnEditorId === editorId ? "by you" : "")
+                                                            }
+                                                        >
+                                                            <LockOutlined
+                                                                fontSize="small"
+                                                                color={dnEditorId === editorId ? "disabled" : "primary"}
+                                                            />
+                                                        </Tooltip>
+                                                    </Grid>
+                                                ) : null}
+                                            </Grid>
+                                        }
+                                        id={`${uniqid}-data`}
+                                        aria-controls={`${uniqid}-dn-tabpanel-data`}
+                                        style={showData ? undefined : noDisplay}
+                                    />
+                                    <Tab
+                                        label="Properties"
+                                        id={`${uniqid}-properties`}
+                                        aria-controls={`${uniqid}-dn-tabpanel-properties`}
+                                    />
+                                    <Tab
+                                        label="History"
+                                        id={`${uniqid}-history`}
+                                        aria-controls={`${uniqid}-dn-tabpanel-history`}
+                                        style={showHistory ? undefined : noDisplay}
+                                    />
+                                </Tabs>
+                                {valid && isFileBased && (fileDownload || fileUpload) ? (
+                                    <Stack direction="row" spacing={1}>
+                                        {fileDownload ? (
+                                            <Tooltip
+                                                title={dnNotDownloadableReason ? dnNotDownloadableReason : "Download"}
+                                            >
+                                                <span>
+                                                    <Button
+                                                        data-action="export"
+                                                        onClick={onfileHandler}
+                                                        sx={buttonSx}
+                                                        disabled={!!dnNotDownloadableReason}
                                                     >
-                                                        <LockOutlined
-                                                            fontSize="small"
-                                                            color={dnEditorId === editorId ? "disabled" : "primary"}
-                                                        />
-                                                    </Tooltip>
-                                                </Grid>
-                                            ) : null}
-                                        </Grid>
-                                    }
-                                    id={`${uniqid}-data`}
-                                    aria-controls={`${uniqid}-dn-tabpanel-data`}
-                                    style={showData ? undefined : noDisplay}
-                                />
-                                <Tab
-                                    label="Properties"
-                                    id={`${uniqid}-properties`}
-                                    aria-controls={`${uniqid}-dn-tabpanel-properties`}
-                                />
-                                <Tab
-                                    label="History"
-                                    id={`${uniqid}-history`}
-                                    aria-controls={`${uniqid}-dn-tabpanel-history`}
-                                    style={showHistory ? undefined : noDisplay}
-                                />
-                            </Tabs>
+                                                        <Download />
+                                                    </Button>
+                                                </span>
+                                            </Tooltip>
+                                        ) : null}
+                                        {fileUpload ? (
+                                            <FileSelector
+                                                hoverText={dnNotUploadableReason ? dnNotUploadableReason : "Upload"}
+                                                icon={<Upload />}
+                                                withBorder={false}
+                                                onUploadAction={props.onFileAction}
+                                                uploadData={uploadData}
+                                                defaultActive={!dnNotUploadableReason}
+                                            />
+                                        ) : null}
+                                    </Stack>
+                                ) : null}
+                            </Stack>
                         </Box>
                         <div
                             role="tabpanel"
@@ -679,10 +766,9 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                             aria-labelledby={`${uniqid}-properties`}
                         >
                             <Grid container rowSpacing={2} sx={gridSx}>
-                                <Grid item xs={12} container justifyContent="space-between" spacing={1}>
+                                <Grid size={12} container justifyContent="space-between" spacing={1}>
                                     <Grid
-                                        item
-                                        xs={12}
+                                        size={12}
                                         container
                                         justifyContent="space-between"
                                         data-focus="label"
@@ -725,10 +811,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                             />
                                         ) : (
                                             <>
-                                                <Grid item xs={4}>
+                                                <Grid size={4}>
                                                     <Typography variant="subtitle2">Label</Typography>
                                                 </Grid>
-                                                <Grid item xs={8}>
+                                                <Grid size={8}>
                                                     <Typography variant="subtitle2">{dnLabel}</Typography>
                                                 </Grid>
                                             </>
@@ -736,41 +822,41 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                     </Grid>
                                 </Grid>
                                 {showEditDate ? (
-                                    <Grid item xs={12} container justifyContent="space-between">
-                                        <Grid item xs={4}>
+                                    <Grid size={12} container justifyContent="space-between">
+                                        <Grid size={4}>
                                             <Typography variant="subtitle2">Last edit date</Typography>
                                         </Grid>
-                                        <Grid item xs={8}>
+                                        <Grid size={8}>
                                             <Typography variant="subtitle2">{dnEditDate}</Typography>
                                         </Grid>
                                     </Grid>
                                 ) : null}
                                 {showExpirationDate ? (
-                                    <Grid item xs={12} container justifyContent="space-between">
-                                        <Grid item xs={4}>
+                                    <Grid size={12} container justifyContent="space-between">
+                                        <Grid size={4}>
                                             <Typography variant="subtitle2">Expiration date</Typography>
                                         </Grid>
-                                        <Grid item xs={8}>
+                                        <Grid size={8}>
                                             <Typography variant="subtitle2">{dnExpirationDate}</Typography>
                                         </Grid>
                                     </Grid>
                                 ) : null}
                                 {showConfig ? (
-                                    <Grid item xs={12} container justifyContent="space-between">
-                                        <Grid item xs={4} pb={2}>
+                                    <Grid size={12} container justifyContent="space-between">
+                                        <Grid size={4} pb={2}>
                                             <Typography variant="subtitle2">Config ID</Typography>
                                         </Grid>
-                                        <Grid item xs={8}>
+                                        <Grid size={8}>
                                             <Typography variant="subtitle2">{dnConfig}</Typography>
                                         </Grid>
                                     </Grid>
                                 ) : null}
                                 {showOwner ? (
-                                    <Grid item xs={12} container justifyContent="space-between">
-                                        <Grid item xs={4}>
+                                    <Grid size={12} container justifyContent="space-between">
+                                        <Grid size={4}>
                                             <Typography variant="subtitle2">Owner</Typography>
                                         </Grid>
-                                        <Grid item xs={7} sx={iconLabelSx}>
+                                        <Grid size={7} sx={iconLabelSx}>
                                             {dnOwnerType === NodeType.CYCLE ? (
                                                 <CycleIcon fontSize="small" color="primary" />
                                             ) : dnOwnerType === NodeType.SCENARIO ? (
@@ -778,7 +864,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                             ) : null}
                                             <Typography variant="subtitle2">{dnOwnerLabel}</Typography>
                                         </Grid>
-                                        <Grid item xs={1}>
+                                        <Grid size={1}>
                                             {dnOwnerId ? (
                                                 <>
                                                     <Tooltip title="Show Scenarios">
@@ -816,7 +902,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                         </Grid>
                                     </Grid>
                                 ) : null}
-                                <Grid item xs={12}>
+                                <Grid size={12}>
                                     <Divider />
                                 </Grid>
                                 <PropertiesEditor
@@ -849,17 +935,17 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                     {props.history.map((edit, idx) => (
                                         <Fragment key={`edit-${idx}`}>
                                             {idx != 0 ? (
-                                                <Grid item xs={12}>
+                                                <Grid size={12}>
                                                     <Divider />
                                                 </Grid>
                                             ) : null}
-                                            <Grid item container>
-                                                <Grid item xs={0.4} sx={editSx}>
+                                            <Grid container>
+                                                <Grid size={0.4} sx={editSx}>
                                                     <Box>{(props.history || []).length - idx}</Box>
                                                 </Grid>
-                                                <Grid item xs={0.1}></Grid>
-                                                <Grid item container xs={11.5}>
-                                                    <Grid item xs={12}>
+                                                <Grid size={0.1}></Grid>
+                                                <Grid container size={11.5}>
+                                                    <Grid size={12}>
                                                         <Typography variant="subtitle1">
                                                             {edit[0]
                                                                 ? format(new Date(edit[0]), editTimestampFormat)
@@ -867,12 +953,12 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                         </Typography>
                                                     </Grid>
                                                     {edit[2] ? (
-                                                        <Grid item xs={12}>
+                                                        <Grid size={12}>
                                                             {edit[2]}
                                                         </Grid>
                                                     ) : null}
                                                     {edit[1] ? (
-                                                        <Grid item xs={12}>
+                                                        <Grid size={12}>
                                                             <Typography fontSize="smaller">{edit[1]}</Typography>
                                                         </Grid>
                                                     ) : null}
@@ -894,9 +980,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                             {dtValue !== undefined ? (
                                 <Grid container justifyContent="space-between" spacing={1}>
                                     <Grid
-                                        item
                                         container
-                                        xs={12}
+                                        size={12}
                                         justifyContent="space-between"
                                         data-focus={dataValueFocus}
                                         onClick={onFocus}
@@ -910,13 +995,13 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                             <>
                                                 {typeof dtValue == "boolean" ? (
                                                     <>
-                                                        <Grid item xs={10}>
+                                                        <Grid size={10}>
                                                             <Switch
                                                                 value={dataValue as boolean}
                                                                 onChange={onDataValueChange}
                                                             />
                                                         </Grid>
-                                                        <Grid item xs={2}>
+                                                        <Grid size={2}>
                                                             <Tooltip title="Apply">
                                                                 <IconButton
                                                                     onClick={editDataValue}
@@ -940,14 +1025,14 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                 ) : dtType == "date" &&
                                                   (dataValue === null || dataValue instanceof Date) ? (
                                                     <LocalizationProvider dateAdapter={AdapterDateFns}>
-                                                        <Grid item xs={10}>
+                                                        <Grid size={10}>
                                                             <DateTimePicker
                                                                 value={dataValue as Date}
                                                                 onChange={onDataValueDateChange}
                                                                 slotProps={textFieldProps}
                                                             />
                                                         </Grid>
-                                                        <Grid item xs={2}>
+                                                        <Grid size={2}>
                                                             <Tooltip title="Apply">
                                                                 <IconButton
                                                                     onClick={editDataValue}
@@ -1018,10 +1103,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                             </>
                                         ) : (
                                             <>
-                                                <Grid item xs={4}>
+                                                <Grid size={4}>
                                                     <Typography variant="subtitle2">Value</Typography>
                                                 </Grid>
-                                                <Grid item xs={8}>
+                                                <Grid size={8}>
                                                     {typeof dtValue == "boolean" ? (
                                                         <Switch
                                                             defaultChecked={dtValue}

+ 15 - 15
frontend/taipy/src/JobSelector.tsx

@@ -22,7 +22,7 @@ import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import Checkbox from "@mui/material/Checkbox";
 import FormControl from "@mui/material/FormControl";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import ListItemText from "@mui/material/ListItemText";
@@ -218,8 +218,8 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                     {form && form.values.filters && form.values.filters.length > 0
                         ? form.values.filters.map((filter, index) => {
                               return (
-                                  <Grid item xs={12} container spacing={2} mb={1} key={index}>
-                                      <Grid item xs={3}>
+                                  <Grid size={12} container spacing={2} mb={1} key={index}>
+                                      <Grid size={3}>
                                           <FormControl fullWidth>
                                               <InputLabel id="data">Column</InputLabel>
                                               <Select
@@ -242,7 +242,7 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                                               </Select>
                                           </FormControl>
                                       </Grid>
-                                      <Grid item xs={3}>
+                                      <Grid size={3}>
                                           <FormControl fullWidth>
                                               <InputLabel id="operator">Operator</InputLabel>
                                               <Select
@@ -256,14 +256,14 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                                               </Select>
                                           </FormControl>
                                       </Grid>
-                                      <Grid item xs={5}>
+                                      <Grid size={5}>
                                           <TextField
                                               label="Value"
                                               variant="outlined"
                                               {...form.getFieldProps(`filters.${index}.value`)}
                                           />
                                       </Grid>
-                                      <Grid item xs={1}>
+                                      <Grid size={1}>
                                           <Tooltip title="Delete Filter">
                                               <IconButton data-idx={index} onClick={removeFilter}>
                                                   <DeleteOutline />
@@ -274,8 +274,8 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                               );
                           })
                         : null}
-                    <Grid item xs={12} container spacing={2} justifyContent="space-between">
-                        <Grid item xs={3}>
+                    <Grid size={12} container spacing={2} justifyContent="space-between">
+                        <Grid size={3}>
                             <FormControl fullWidth>
                                 <InputLabel id="data-new">Column</InputLabel>
                                 <Select
@@ -298,7 +298,7 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                                 </Select>
                             </FormControl>
                         </Grid>
-                        <Grid item xs={3}>
+                        <Grid size={3}>
                             <FormControl fullWidth>
                                 <InputLabel id="operator-new">Operator</InputLabel>
                                 <Select
@@ -312,10 +312,10 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                                 </Select>
                             </FormControl>
                         </Grid>
-                        <Grid item xs={5}>
+                        <Grid size={5}>
                             <TextField label="Value" variant="outlined" {...form.getFieldProps(`newValue`)} />
                         </Grid>
-                        <Grid item xs={1}>
+                        <Grid size={1}>
                             <Tooltip
                                 title={typeof form.values.newData === "string" ? "Cannot Add Filter" : "Add Filter"}
                             >
@@ -325,7 +325,7 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
                             </Tooltip>
                         </Grid>
                     </Grid>
-                    <Grid item xs={12} container justifyContent="space-between" mt={2}>
+                    <Grid size={12} container justifyContent="space-between" mt={2}>
                         <Button
                             variant="outlined"
                             color="inherit"
@@ -810,7 +810,7 @@ const JobSelector = (props: JobSelectorProps) => {
             <Paper sx={containerSx}>
                 <Toolbar sx={headerToolbarSx}>
                     <Grid container spacing={2} alignItems="center">
-                        <Grid item container xs={3} alignItems="center">
+                        <Grid container size={3} alignItems="center">
                             <Tooltip title="Filter">
                                 <IconButton onClick={handleFilterOpen}>
                                     <FilterList />
@@ -824,10 +824,10 @@ const JobSelector = (props: JobSelectorProps) => {
                         </Grid>
                         {checked.length ? (
                             <>
-                                <Grid item xs={7}>
+                                <Grid size={7}>
                                     <Typography variant="subtitle1">{checked.length} selected</Typography>
                                 </Grid>
-                                <Grid item container justifyContent="flex-end" spacing={1} xs={2}>
+                                <Grid container justifyContent="flex-end" spacing={1} size={2}>
                                     {showCancel ? (
                                         <Tooltip title="Cancel Jobs">
                                             <span>

+ 18 - 18
frontend/taipy/src/JobViewer.tsx

@@ -14,7 +14,7 @@
 import React, { useEffect, useCallback } from "react";
 import Button from "@mui/material/Button";
 import Divider from "@mui/material/Divider";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import ListItemText from "@mui/material/ListItemText";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
@@ -101,35 +101,35 @@ const JobViewer = (props: JobViewerProps) => {
         <Grid container className={className} sx={{ maxWidth: width }}>
             {inDialog ? null : (
                 <>
-                    <Grid item xs={4}>
+                    <Grid size={4}>
                         <Typography>Job Name</Typography>
                     </Grid>
-                    <Grid item xs={8}>
+                    <Grid size={8}>
                         <Typography>{jobName}</Typography>
                     </Grid>
                     <Divider />
                 </>
             )}
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Job Id</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <Tooltip title={jobId}>
                     <Typography sx={EllipsisSx}>{jobId}</Typography>
                 </Tooltip>
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Submission Id</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <Tooltip title={submissionId}>
                     <Typography sx={EllipsisSx}>{submissionId}</Typography>
                 </Tooltip>
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Submitted entity</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <Tooltip title={entityId}>
                     <ListItemText
                         primary={entityName}
@@ -138,29 +138,29 @@ const JobViewer = (props: JobViewerProps) => {
                     />
                 </Tooltip>
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Execution time</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <Typography>{executionTime}</Typography>
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Status</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <StatusChip status={status} />
             </Grid>
-            <Grid item xs={4}>
+            <Grid size={4}>
                 <Typography>Creation date</Typography>
             </Grid>
-            <Grid item xs={8}>
+            <Grid size={8}>
                 <Typography>{creationDate ? new Date(creationDate).toLocaleString() : ""}</Typography>
             </Grid>
             <Divider />
-            <Grid item xs={12}>
+            <Grid size={12}>
                 <Typography>Stack Trace</Typography>
             </Grid>
-            <Grid item xs={12}>
+            <Grid size={12}>
                 <Typography variant="caption" component="pre" overflow="auto" maxHeight="50vh">
                     {stacktrace.join("<br/>")}
                 </Typography>
@@ -168,7 +168,7 @@ const JobViewer = (props: JobViewerProps) => {
             {props.onDelete ? (
                 <>
                     <Divider />
-                    <Grid item xs={6}>
+                    <Grid size={6}>
                         <Tooltip title={notDeleteable}>
                             <span>
                                 <Button variant="outlined" onClick={handleDeleteJob} disabled={!!notDeleteable}>

+ 19 - 30
frontend/taipy/src/PropertiesEditor.tsx

@@ -13,7 +13,7 @@
 
 import React, { useState, useCallback, useEffect, ChangeEvent, MouseEvent, KeyboardEvent } from "react";
 import Divider from "@mui/material/Divider";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
@@ -179,14 +179,13 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
 
     return show ? (
         <>
-            <Grid item xs={12} container rowSpacing={2}>
+            <Grid size={12} container rowSpacing={2}>
                 {properties
                     ? properties.map((property) => {
                           const propName = `property-${property.id}`;
                           return (
                               <Grid
-                                  item
-                                  xs={12}
+                                  size={12}
                                   spacing={1}
                                   container
                                   justifyContent="space-between"
@@ -197,7 +196,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                               >
                                   {active && !notEditableReason && focusName === propName ? (
                                       <>
-                                          <Grid item xs={4}>
+                                          <Grid size={4}>
                                               <TextField
                                                   label="Key"
                                                   variant="outlined"
@@ -210,7 +209,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                                   inputProps={{ onKeyDown }}
                                               />
                                           </Grid>
-                                          <Grid item xs={5}>
+                                          <Grid size={5}>
                                               <TextField
                                                   label="Value"
                                                   variant="outlined"
@@ -224,8 +223,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                               />
                                           </Grid>
                                           <Grid
-                                              item
-                                              xs={2}
+                                              size={2}
                                               container
                                               alignContent="center"
                                               alignItems="center"
@@ -253,8 +251,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                               </Tooltip>
                                           </Grid>
                                           <Grid
-                                              item
-                                              xs={1}
+                                              size={1}
                                               container
                                               alignContent="center"
                                               alignItems="center"
@@ -279,13 +276,13 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                       </>
                                   ) : (
                                       <>
-                                          <Grid item xs={4}>
+                                          <Grid size={4}>
                                               <Typography variant="subtitle2">{property.key}</Typography>
                                           </Grid>
-                                          <Grid item xs={5}>
+                                          <Grid size={5}>
                                               <Typography variant="subtitle2">{property.value}</Typography>
                                           </Grid>
-                                          <Grid item xs={3} />
+                                          <Grid size={3} />
                                       </>
                                   )}
                               </Grid>
@@ -293,8 +290,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                       })
                     : null}
                 <Grid
-                    item
-                    xs={12}
+                    size={12}
                     spacing={1}
                     container
                     justifyContent="space-between"
@@ -304,7 +300,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                 >
                     {active && focusName == "new-property" ? (
                         <>
-                            <Grid item xs={4}>
+                            <Grid size={4}>
                                 <TextField
                                     value={newProp.key}
                                     data-name="key"
@@ -316,7 +312,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                     inputProps={{ onKeyDown }}
                                 />
                             </Grid>
-                            <Grid item xs={5}>
+                            <Grid size={5}>
                                 <TextField
                                     value={newProp.value}
                                     data-name="value"
@@ -328,14 +324,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                     inputProps={{ onKeyDown, "data-enter": true }}
                                 />
                             </Grid>
-                            <Grid
-                                item
-                                xs={2}
-                                container
-                                alignContent="center"
-                                alignItems="center"
-                                justifyContent="center"
-                            >
+                            <Grid size={2} container alignContent="center" alignItems="center" justifyContent="center">
                                 <Tooltip title="Apply">
                                     <IconButton sx={IconPaddingSx} onClick={editProperty} size="small">
                                         <CheckCircle color="primary" />
@@ -347,22 +336,22 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                     </IconButton>
                                 </Tooltip>
                             </Grid>
-                            <Grid item xs={1} />
+                            <Grid size={1} />
                         </>
                     ) : (
                         <>
-                            <Grid item xs={4}>
+                            <Grid size={4}>
                                 <Typography variant="subtitle2">New Property Key</Typography>
                             </Grid>
-                            <Grid item xs={5}>
+                            <Grid size={5}>
                                 <Typography variant="subtitle2">Value</Typography>
                             </Grid>
-                            <Grid item xs={3} />
+                            <Grid size={3} />
                         </>
                     )}
                 </Grid>
             </Grid>
-            <Grid item xs={12}>
+            <Grid size={12}>
                 <Divider />
             </Grid>
         </>

+ 17 - 17
frontend/taipy/src/ScenarioSelector.tsx

@@ -22,7 +22,7 @@ import DialogTitle from "@mui/material/DialogTitle";
 import FormControl from "@mui/material/FormControl";
 import FormGroup from "@mui/material/FormGroup";
 import FormHelperText from "@mui/material/FormHelperText";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import MenuItem from "@mui/material/MenuItem";
@@ -248,7 +248,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                 <form onSubmit={form.handleSubmit}>
                     <DialogContent sx={DialogContentSx} dividers>
                         <Grid container rowSpacing={2}>
-                            <Grid item xs={12}>
+                            <Grid size={12}>
                                 <FormGroup>
                                     <FormControl fullWidth>
                                         <InputLabel id="select-config">Configuration</InputLabel>
@@ -276,7 +276,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                     </FormControl>
                                 </FormGroup>
                             </Grid>
-                            <Grid item xs={12}>
+                            <Grid size={12}>
                                 <FormGroup>
                                     <TextField
                                         {...form.getFieldProps("name")}
@@ -287,7 +287,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                     />
                                 </FormGroup>
                             </Grid>
-                            <Grid item xs={12}>
+                            <Grid size={12}>
                                 <FormGroup>
                                     <LocalizationProvider dateAdapter={AdapterDateFns}>
                                         <DatePicker
@@ -301,13 +301,13 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                     </LocalizationProvider>
                                 </FormGroup>
                             </Grid>
-                            <Grid item xs={12} container justifyContent="space-between">
+                            <Grid size={12} container justifyContent="space-between">
                                 <Typography variant="h6">Custom Properties</Typography>
                             </Grid>
                             {properties
                                 ? properties.map((item, index) => (
-                                      <Grid item xs={12} key={item.id} container spacing={1} alignItems="end">
-                                          <Grid item xs={4}>
+                                      <Grid size={12} key={item.id} container spacing={1} alignItems="end">
+                                          <Grid size={4}>
                                               <TextField
                                                   value={item.key}
                                                   label="Key"
@@ -317,7 +317,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                                   onChange={updatePropertyField}
                                               />
                                           </Grid>
-                                          <Grid item xs>
+                                          <Grid size="grow">
                                               <TextField
                                                   value={item.value}
                                                   label="Value"
@@ -327,7 +327,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                                   onChange={updatePropertyField}
                                               />
                                           </Grid>
-                                          <Grid item xs="auto">
+                                          <Grid size="auto">
                                               <Tooltip title="Delete Property">
                                                   <Button
                                                       variant="outlined"
@@ -343,8 +343,8 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                       </Grid>
                                   ))
                                 : null}
-                            <Grid item xs={12} container spacing={1} justifyContent="space-between" alignItems="end">
-                                <Grid item xs={4}>
+                            <Grid size={12} container spacing={1} justifyContent="space-between" alignItems="end">
+                                <Grid size={4}>
                                     <TextField
                                         value={newProp.key}
                                         data-name="key"
@@ -353,7 +353,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                         variant="outlined"
                                     />
                                 </Grid>
-                                <Grid item xs>
+                                <Grid size="grow">
                                     <TextField
                                         value={newProp.value}
                                         data-name="value"
@@ -362,7 +362,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                         variant="outlined"
                                     />
                                 </Grid>
-                                <Grid item xs="auto">
+                                <Grid size="auto">
                                     <Tooltip title="Add Property">
                                         <span>
                                             <Button
@@ -383,7 +383,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                     <DialogActions>
                         <Grid container justifyContent="space-between" sx={ActionContentSx}>
                             {actionEdit && (
-                                <Grid item xs={6}>
+                                <Grid size={6}>
                                     <Button
                                         variant="outlined"
                                         color="error"
@@ -394,13 +394,13 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                     </Button>
                                 </Grid>
                             )}
-                            <Grid item container xs={actionEdit ? 6 : 12} justifyContent="flex-end">
-                                <Grid item sx={CancelBtnSx}>
+                            <Grid container size={actionEdit ? 6 : 12} justifyContent="flex-end">
+                                <Grid sx={CancelBtnSx}>
                                     <Button variant="outlined" color="inherit" onClick={close}>
                                         Cancel
                                     </Button>
                                 </Grid>
-                                <Grid item>
+                                <Grid>
                                     <Button
                                         variant="contained"
                                         type="submit"

+ 57 - 68
frontend/taipy/src/ScenarioViewer.tsx

@@ -20,12 +20,14 @@ import Chip from "@mui/material/Chip";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import Divider from "@mui/material/Divider";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import InputAdornment from "@mui/material/InputAdornment";
+import Stack from "@mui/material/Stack";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
+
 import Add from "@mui/icons-material/Add";
 import ArrowForwardIosSharp from "@mui/icons-material/ArrowForwardIosSharp";
 import Cancel from "@mui/icons-material/Cancel";
@@ -201,10 +203,10 @@ const SequenceRow = ({
     const disabledSubmit = disabled || !!notSubmittableReason;
 
     return (
-        <Grid item xs={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
+        <Grid size={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
             {active && !notEditableReason && focusName === name ? (
                 <>
-                    <Grid item xs={4}>
+                    <Grid size={4}>
                         <TextField
                             label={`Sequence ${number + 1}`}
                             variant="outlined"
@@ -217,7 +219,7 @@ const SequenceRow = ({
                             helperText={valid ? "" : label ? "This name is already used." : "Cannot be empty."}
                         />
                     </Grid>
-                    <Grid item xs={4}>
+                    <Grid size={4}>
                         <Autocomplete
                             multiple
                             options={Object.keys(tasks)}
@@ -250,7 +252,7 @@ const SequenceRow = ({
                             disabled={disabled}
                         />
                     </Grid>
-                    <Grid item xs={2} container alignContent="center" alignItems="center" justifyContent="center">
+                    <Grid size={2} container alignContent="center" alignItems="center" justifyContent="center">
                         <Tooltip title="Apply">
                             <IconButton sx={IconPaddingSx} onClick={onSaveSequence} size="small" disabled={!valid}>
                                 <CheckCircle color={disableColor("primary", !valid)} />
@@ -265,15 +267,15 @@ const SequenceRow = ({
                 </>
             ) : (
                 <>
-                    <Grid item xs={5}>
+                    <Grid size={5}>
                         <Typography variant="subtitle2">{label || "New Sequence"}</Typography>
                     </Grid>
-                    <Grid item xs={5}>
+                    <Grid size={5}>
                         {taskIds.map((id) =>
                             tasks[id] ? <Chip key={id} label={tasks[id]} variant="outlined" /> : null
                         )}
                     </Grid>
-                    <Grid item xs={1} alignContent="center" alignItems="center" justifyContent="center">
+                    <Grid size={1} alignContent="center" alignItems="center" justifyContent="center">
                         <Tooltip title={`Delete Sequence '${label}'`}>
                             <span>
                                 <IconButton size="small" onClick={onDeleteSequence} disabled={disabled}>
@@ -282,7 +284,7 @@ const SequenceRow = ({
                             </span>
                         </Tooltip>
                     </Grid>
-                    <Grid item xs={1} alignContent="center" alignItems="center" justifyContent="center">
+                    <Grid size={1} alignContent="center" alignItems="center" justifyContent="center">
                         {pLabel && submit ? (
                             <Tooltip
                                 title={
@@ -627,16 +629,9 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                         expandIcon={expandable ? <ArrowForwardIosSharp sx={AccordionIconSx} /> : null}
                         sx={AccordionSummarySx}
                     >
-                        <Grid
-                            container
-                            alignItems="center"
-                            direction="row"
-                            flexWrap="nowrap"
-                            justifyContent="space-between"
-                            spacing={1}
-                        >
-                            <Grid item>
-                                {scLabel}
+                        <Stack direction="row" justifyContent="space-between" width="100%" alignItems="center">
+                            <Stack direction="row" spacing={1}>
+                                <Typography>{scLabel}</Typography>
                                 {scPrimary ? (
                                     <Chip
                                         color="primary"
@@ -646,68 +641,63 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                     />
                                 ) : null}
                                 {submissionStatus > -1 ? <StatusChip status={submissionStatus} sx={ChipSx} /> : null}
-                            </Grid>
-                            <Grid item>
-                                {showSubmit ? (
-                                    <Tooltip
-                                        title={
-                                            disabled
-                                                ? scNotSubmittableReason || "Cannot submit Scenario"
-                                                : "Submit Scenario"
-                                        }
-                                    >
-                                        <span>
-                                            <Button
-                                                onClick={submitScenario}
-                                                disabled={disabled}
-                                                endIcon={
-                                                    <Send fontSize="medium" color={disableColor("info", disabled)} />
-                                                }
-                                            >
-                                                Submit
-                                            </Button>
-                                        </span>
-                                    </Tooltip>
-                                ) : null}
-                            </Grid>
-                        </Grid>
+                            </Stack>
+                            {showSubmit ? (
+                                <Tooltip
+                                    title={
+                                        disabled
+                                            ? scNotSubmittableReason || "Cannot submit Scenario"
+                                            : "Submit Scenario"
+                                    }
+                                >
+                                    <span>
+                                        <Button
+                                            onClick={submitScenario}
+                                            disabled={disabled}
+                                            endIcon={<Send fontSize="medium" color={disableColor("info", disabled)} />}
+                                        >
+                                            Submit
+                                        </Button>
+                                    </span>
+                                </Tooltip>
+                            ) : null}
+                        </Stack>
                     </AccordionSummary>
                     <AccordionDetails>
                         <Grid container rowSpacing={2}>
                             {showConfig ? (
-                                <Grid item xs={12} container justifyContent="space-between">
-                                    <Grid item xs={4} pb={2}>
+                                <Grid size={12} container justifyContent="space-between">
+                                    <Grid size={4} pb={2}>
                                         <Typography variant="subtitle2">Config ID</Typography>
                                     </Grid>
-                                    <Grid item xs={8}>
+                                    <Grid size={8}>
                                         <Typography variant="subtitle2">{scConfig}</Typography>
                                     </Grid>
                                 </Grid>
                             ) : null}
                             {showCreationDate ? (
-                                <Grid item xs={12} container justifyContent="space-between">
-                                    <Grid item xs={4}>
+                                <Grid size={12} container justifyContent="space-between">
+                                    <Grid size={4}>
                                         <Typography variant="subtitle2">Creation Date</Typography>
                                     </Grid>
-                                    <Grid item xs={8}>
+                                    <Grid size={8}>
                                         <Typography variant="subtitle2">{scCreationDate}</Typography>
                                     </Grid>
                                 </Grid>
                             ) : null}
                             {showCycle ? (
-                                <Grid item xs={12} container justifyContent="space-between">
-                                    <Grid item xs={4}>
+                                <Grid size={12} container justifyContent="space-between">
+                                    <Grid size={4}>
                                         <Typography variant="subtitle2">Cycle / Frequency</Typography>
                                     </Grid>
-                                    <Grid item xs={8}>
+                                    <Grid size={8}>
                                         <Typography variant="subtitle2">{scCycle}</Typography>
                                     </Grid>
                                 </Grid>
                             ) : null}
-                            <Grid item xs={12} container justifyContent="space-between" spacing={1}>
+                            <Grid size={12} container justifyContent="space-between" spacing={1}>
                                 <Grid
-                                    item
-                                    xs={12}
+                                    size={12}
                                     container
                                     justifyContent="space-between"
                                     data-focus="label"
@@ -751,10 +741,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         />
                                     ) : (
                                         <>
-                                            <Grid item xs={4}>
+                                            <Grid size={4}>
                                                 <Typography variant="subtitle2">Label</Typography>
                                             </Grid>
-                                            <Grid item xs={8}>
+                                            <Grid size={8}>
                                                 <Typography variant="subtitle2">{scLabel}</Typography>
                                             </Grid>
                                         </>
@@ -762,8 +752,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 </Grid>
                                 {showTags ? (
                                     <Grid
-                                        item
-                                        xs={12}
+                                    size={12}
                                         container
                                         justifyContent="space-between"
                                         data-focus="tags"
@@ -830,10 +819,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                             />
                                         ) : (
                                             <>
-                                                <Grid item xs={4}>
+                                                <Grid size={4}>
                                                     <Typography variant="subtitle2">Tags</Typography>
                                                 </Grid>
-                                                <Grid item xs={8}>
+                                                <Grid size={8}>
                                                     {tags.map((tag, index) => (
                                                         <Chip key={index} label={tag} variant="outlined" />
                                                     ))}
@@ -844,7 +833,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 ) : null}
                             </Grid>
 
-                            <Grid item xs={12}>
+                            <Grid size={12}>
                                 <Divider />
                             </Grid>
                             <PropertiesEditor
@@ -862,11 +851,11 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                             />
                             {showSequences ? (
                                 <>
-                                    <Grid item xs={12} container justifyContent="space-between">
-                                        <Grid item xs={9}>
+                                    <Grid size={12} container justifyContent="space-between">
+                                        <Grid size={9}>
                                             <Typography variant="h6">Sequences</Typography>
                                         </Grid>
-                                        <Grid item xs={3} sx={{ ml: "auto" }}>
+                                        <Grid size={3} sx={{ ml: "auto" }}>
                                             <Button onClick={addSequenceHandler} endIcon={<Add />}>
                                                 Add
                                             </Button>
@@ -897,12 +886,12 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         );
                                     })}
 
-                                    <Grid item xs={12}>
+                                    <Grid size={12}>
                                         <Divider />
                                     </Grid>
                                 </>
                             ) : null}
-                            <Grid item xs={12} container justifyContent="space-between">
+                            <Grid size={12} container justifyContent="space-between">
                                 {showDelete ? (
                                     <Button
                                         variant="outlined"

+ 1 - 1
frontend/taipy/src/utils/ConfirmDialog.tsx

@@ -4,7 +4,7 @@ import DialogActions from "@mui/material/DialogActions";
 import DialogContent from "@mui/material/DialogContent";
 import DialogTitle from "@mui/material/DialogTitle";
 import Button from "@mui/material/Button";
-import Grid from "@mui/material/Grid";
+import Grid from "@mui/material/Grid2";
 import IconButton from "@mui/material/IconButton";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";

+ 23 - 1
taipy/core/data/_file_datanode_mixin.py

@@ -20,7 +20,7 @@ from taipy.config.config import Config
 from taipy.logger._taipy_logger import _TaipyLogger
 
 from .._entity._reload import _self_reload
-from ..reason import InvalidUploadFile, ReasonCollection, UploadFileCanNotBeRead
+from ..reason import InvalidUploadFile, NoFileToDownload, NotAFile, ReasonCollection, UploadFileCanNotBeRead
 from .data_node import DataNode
 from .data_node_id import Edit
 
@@ -97,6 +97,28 @@ class _FileDataNodeMixin(object):
             shutil.move(old_path, new_path)
         return new_path
 
+    def is_downloadable(self) -> ReasonCollection:
+        """Check if the data node is downloadable.
+
+        Returns:
+            A `ReasonCollection^` object containing the reasons why the data node is not downloadable.
+        """
+        collection = ReasonCollection()
+        if not os.path.exists(self.path):
+            collection._add_reason(self.id, NoFileToDownload(self.path, self.id))  # type: ignore[attr-defined]
+        elif not isfile(self.path):
+            collection._add_reason(self.id, NotAFile(self.path, self.id))  # type: ignore[attr-defined]
+        return collection
+
+    def is_uploadable(self) -> ReasonCollection:
+        """Check if the data node is uploadable.
+
+        Returns:
+            A `ReasonCollection^` object containing the reasons why the data node is not uploadable.
+        """
+
+        return ReasonCollection()
+
     def _get_downloadable_path(self) -> str:
         """Get the downloadable path of the file data of the data node.
 

+ 1 - 1
taipy/core/notification/__init__.py

@@ -10,7 +10,7 @@
 # specific language governing permissions and limitations under the License.
 
 """
-Package for notifications about changes on `Core^` service entities.
+Package for notifications about changes on `Orchestrator^` service entities.
 
 
 The Core service generates `Event^` objects to track changes on entities.

+ 2 - 2
taipy/core/notification/notifier.py

@@ -51,7 +51,7 @@ def _publish_event(
 
 
 class Notifier:
-    """A class for managing event registrations and publishing `Core^` service events."""
+    """A class for managing event registrations and publishing `Orchestrator^` service events."""
 
     _topics_registrations_list: Dict[_Topic, Set[_Registration]] = {}
 
@@ -166,7 +166,7 @@ class Notifier:
 
     @classmethod
     def publish(cls, event) -> None:
-        """Publish a `Core^` service event to all registered listeners whose topic matches the event.
+        """Publish a `Orchestrator^` service event to all registered listeners whose topic matches the event.
 
         Parameters:
             event (Event^): The event to publish.

+ 2 - 0
taipy/core/reason/__init__.py

@@ -16,6 +16,8 @@ from .reason import (
     EntityIsNotSubmittableEntity,
     InvalidUploadFile,
     JobIsNotFinished,
+    NoFileToDownload,
+    NotAFile,
     NotGlobalScope,
     Reason,
     ScenarioDoesNotBelongToACycle,

+ 28 - 0
taipy/core/reason/reason.py

@@ -143,6 +143,34 @@ class UploadFileCanNotBeRead(Reason, _DataNodeReasonMixin):
         _DataNodeReasonMixin.__init__(self, datanode_id)
 
 
+class NoFileToDownload(Reason, _DataNodeReasonMixin):
+    """
+    There is no file to download, therefore the download action cannot be performed.
+
+    Attributes:
+        datanode_id (str): The id of the data node that the file is intended to download from.
+    """
+
+    def __init__(self, file_path: str, datanode_id: str):
+        Reason.__init__(self, f"Path '{file_path}' from data node '{datanode_id}'"
+                              f" does not exist and cannot be downloaded.")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class NotAFile(Reason, _DataNodeReasonMixin):
+    """
+    The data node path is not a file, therefore the download action cannot be performed.
+
+    Attributes:
+        datanode_id (str): The datanode id that the file is intended to download from.
+    """
+
+    def __init__(self, file_path: str, datanode_id: str):
+        Reason.__init__(self, f"Path '{file_path}' from data node '{datanode_id}'"
+                              f" is not a file and can t be downloaded.")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
 class InvalidUploadFile(Reason, _DataNodeReasonMixin):
     """
     The uploaded file has invalid data, therefore is not a valid data file for the data node.

+ 2 - 3
taipy/core/submission/_submission_manager.py

@@ -64,8 +64,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
 
             job_status = job.status
             if job_status == Status.FAILED:
-                submission._submission_status = SubmissionStatus.FAILED
-                cls._set(submission)
+                cls.__set_submission_status(submission, SubmissionStatus.FAILED, job)
                 cls.__logger.debug(
                     f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`."
                 )
@@ -110,7 +109,7 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
             else:
                 cls.__set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
             cls.__logger.debug(
-                f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`"
+                f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`."
             )
 
     @classmethod

+ 1 - 0
taipy/gui/_default_config.py

@@ -59,6 +59,7 @@ default_config: Config = {
     "notebook_proxy": True,
     "notification_duration": 3000,
     "port": 5000,
+    "port_auto_ranges": [(49152, 65535)],
     "propagate": True,
     "run_browser": True,
     "run_in_thread": False,

+ 12 - 1
taipy/gui/_renderers/_markdown/preproc.py

@@ -60,6 +60,9 @@ class _Preprocessor(MdPreprocessor):
     #  Note 2: Space characters after the equal sign are significative
     __PROPERTY_RE = re.compile(r"((?:don'?t|not)\s+)?([a-zA-Z][\.a-zA-Z_$0-9]*(?:\[(?:.*?)\])?)\s*(?:=(.*))?$")
 
+    # Error syntax detection regex
+    __MISSING_LEADING_PIPE_RE = re.compile(r"<[^|](.*?)\|>")
+
     _gui: "Gui"
 
     @staticmethod
@@ -72,10 +75,18 @@ class _Preprocessor(MdPreprocessor):
         # Un-escape pipe character in property value
         return (prop_name, prop_value.replace("\\|", "|"))
 
+    def _validate_line(self, line: str, line_count: int) -> bool:
+        if _Preprocessor.__MISSING_LEADING_PIPE_RE.search(line) is not None:
+            _warn(f"Missing leading pipe '|' in opening tag line {line_count}: '{line}'.")
+            return False
+        return True
+
     def run(self, lines: List[str]) -> List[str]:
         new_lines = []
         tag_stack = []
         for line_count, line in enumerate(lines, start=1):
+            if not self._validate_line(line, line_count):
+                continue
             new_line = ""
             last_index = 0
             # Opening tags
@@ -100,7 +111,7 @@ class _Preprocessor(MdPreprocessor):
                         line += f' {property[0]}="{prop_value}"'
                     line += _MarkdownFactory._TAIPY_END + new_line_delimeter
                 else:
-                    _warn(f"Invalid tag name '{tag}' in line {line_count}.")
+                    _warn(f"Failed to recognized block tag '{tag}' in line {line_count}. Check that you are closing the tag properly with '|>' if it is a control element.")  # noqa: E501
             # Other controls
             for m in _Preprocessor.__CONTROL_RE.finditer(line):
                 control_name, properties = self._process_control(m.group(1), line_count)

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

@@ -406,6 +406,7 @@ class _Factory:
                 ("on_action", PropertyType.function),
                 ("label",),
                 ("change_delay", PropertyType.number, gui._get_config("change_delay", None)),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "pane": lambda gui, control_type, attrs: _Builder(
@@ -437,6 +438,7 @@ class _Factory:
                 ("render", PropertyType.dynamic_boolean, True),
                 ("height", PropertyType.dynamic_string),
                 ("content", PropertyType.toHtmlContent),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "selector": lambda gui, control_type, attrs: _Builder(

+ 2 - 0
taipy/gui/config.py

@@ -47,6 +47,7 @@ ConfigParameter = t.Literal[
     "notebook_proxy",
     "notification_duration",
     "port",
+    "port_auto_ranges",
     "propagate",
     "run_browser",
     "run_in_thread",
@@ -119,6 +120,7 @@ Config = t.TypedDict(
         "notebook_proxy": bool,
         "notification_duration": int,
         "port": t.Union[t.Literal["auto"], int],
+        "port_auto_ranges": t.List[t.Union[int, t.Tuple[int, int]]],
         "propagate": bool,
         "run_browser": bool,
         "run_in_thread": bool,

+ 1 - 2
taipy/gui/data/utils.py

@@ -50,8 +50,7 @@ class Decimator(ABC):
 
     def _is_applicable(self, data: t.Any, nb_rows_max: int, chart_mode: str):
         if chart_mode not in self._CHART_MODES:
-            _warn(f"{type(self).__name__} is only applicable for {' '.join(self._CHART_MODES)}.")
-            return False
+            _warn(f"Decimator '{type(self).__name__}' is not optimized for chart mode '{chart_mode}'. Consider using other chart mode such as '{f'{chr(39)}, {chr(39)}'.join(self._CHART_MODES)}.'")  # noqa: E501
         if self.threshold is None:
             if nb_rows_max < len(data):
                 return True

+ 32 - 14
taipy/gui/gui.py

@@ -965,20 +965,23 @@ class Gui:
 
     def __upload_files(self):
         self.__set_client_id_in_context()
-        if "var_name" not in request.form:
-            _warn("No var name")
-            return ("No var name", 400)
-        var_name = request.form["var_name"]
+        on_upload_action = request.form.get("on_action", None)
+        var_name = request.form.get("var_name", None)
+        if not var_name and not on_upload_action:
+            _warn("upload files: No var name")
+            return ("upload files: No var name", 400)
+        context = request.form.get("context", None)
+        upload_data = request.form.get("upload_data", None)
         multiple = "multiple" in request.form and request.form["multiple"] == "True"
-        if "blob" not in request.files:
-            _warn("No file part")
-            return ("No file part", 400)
-        file = request.files["blob"]
+        file = request.files.get("blob", None)
+        if not file:
+            _warn("upload files: No file part")
+            return ("upload files: No file part", 400)
         # If the user does not select a file, the browser submits an
         # empty file without a filename.
         if file.filename == "":
-            _warn("No selected file")
-            return ("No selected file", 400)
+            _warn("upload files: No selected file")
+            return ("upload files: No selected file", 400)
         suffix = ""
         complete = True
         part = 0
@@ -1007,13 +1010,27 @@ class Gui:
                         return (f"Cannot group file after chunk upload for {file.filename}", 500)
                 # notify the file is uploaded
                 newvalue = str(file_path)
-                if multiple:
+                if multiple and var_name:
                     value = _getscopeattr(self, var_name)
                     if not isinstance(value, t.List):
                         value = [] if value is None else [value]
                     value.append(newvalue)
                     newvalue = value
-                setattr(self._bindings(), var_name, newvalue)
+                with self._set_locals_context(context):
+                    if on_upload_action:
+                        data = {}
+                        if upload_data:
+                            try:
+                                data = json.loads(upload_data)
+                            except Exception:
+                                pass
+                        data["path"] = file_path
+                        file_fn = self._get_user_function(on_upload_action)
+                        if not callable(file_fn):
+                            file_fn = _getscopeattr(self, on_upload_action)
+                        self._call_function_with_state(file_fn, ["file_upload", {"args": [data]}])
+                    else:
+                        setattr(self._bindings(), var_name, newvalue)
         return ("", 200)
 
     _data_request_counter = 1
@@ -2398,8 +2415,8 @@ class Gui:
             elif script_file.is_dir() and (script_file / "taipy.css").exists():
                 css_file = "taipy.css"
         if css_file is None:
-             script_file = script_file.with_name("taipy").with_suffix(".css")
-             if script_file.exists():
+            script_file = script_file.with_name("taipy").with_suffix(".css")
+            if script_file.exists():
                 css_file = f"{script_file.stem}.css"
         self.__css_file = css_file
 
@@ -2751,6 +2768,7 @@ class Gui:
             run_in_thread=app_config["run_in_thread"],
             allow_unsafe_werkzeug=app_config["allow_unsafe_werkzeug"],
             notebook_proxy=app_config["notebook_proxy"],
+            port_auto_ranges=app_config["port_auto_ranges"]
         )
 
     def reload(self):  # pragma: no cover

+ 24 - 6
taipy/gui/server.py

@@ -21,7 +21,7 @@ import time
 import typing as t
 import webbrowser
 from importlib import util
-from random import randint
+from random import choices, randint
 
 from flask import Blueprint, Flask, json, jsonify, render_template, request, send_from_directory
 from flask_cors import CORS
@@ -258,7 +258,7 @@ class _Server:
         if self._get_async_mode() == "gevent" and util.find_spec("gevent"):
             from gevent import get_hub, monkey
 
-            get_hub().NOT_ERROR += (KeyboardInterrupt, )
+            get_hub().NOT_ERROR += (KeyboardInterrupt,)
             if not monkey.is_module_patched("time"):
                 monkey.patch_time()
         if self._get_async_mode() == "eventlet" and util.find_spec("eventlet"):
@@ -267,17 +267,35 @@ class _Server:
             if not patcher.is_monkey_patched("time"):
                 monkey_patch(time=True)
 
-    def _get_random_port(self):  # pragma: no cover
+    def _get_random_port(
+        self, port_auto_ranges: t.Optional[t.List[t.Union[int, t.Tuple[int, int]]]] = None
+    ):  # pragma: no cover
+        port_auto_ranges = port_auto_ranges or [(49152, 65535)]
+        random_weights = [1 if isinstance(r, int) else abs(r[1] - r[0]) + 1 for r in port_auto_ranges]
         while True:
-            port = randint(49152, 65535)
+            random_choices = [
+                r if isinstance(r, int) else randint(min(r[0], r[1]), max(r[0], r[1])) for r in port_auto_ranges
+            ]
+            port = choices(random_choices, weights=random_weights)[0]
             if port not in _RuntimeManager().get_used_port() and not _is_port_open(self._host, port):
                 return port
 
-    def run(self, host, port, debug, use_reloader, flask_log, run_in_thread, allow_unsafe_werkzeug, notebook_proxy):
+    def run(
+        self,
+        host,
+        port,
+        debug,
+        use_reloader,
+        flask_log,
+        run_in_thread,
+        allow_unsafe_werkzeug,
+        notebook_proxy,
+        port_auto_ranges,
+    ):
         host_value = host if host != "0.0.0.0" else "localhost"
         self._host = host
         if port == "auto":
-            port = self._get_random_port()
+            port = self._get_random_port(port_auto_ranges)
         self._port = port
         if _is_in_notebook() and notebook_proxy:  # pragma: no cover
             from .utils.proxy import NotebookProxy

+ 5 - 2
taipy/gui/utils/_evaluator.py

@@ -91,7 +91,8 @@ class _Evaluator:
         var_val: t.Dict[str, t.Any] = {}
         var_map: t.Dict[str, str] = {}
         non_vars = list(self.__global_ctx.keys())
-        non_vars.extend(dir(builtins))
+        builtin_vars = dir(builtins)
+        non_vars.extend(builtin_vars)
         # Get a list of expressions (value that has been wrapped in curly braces {}) and find variables to bind
         for e in self._fetch_expression_list(expr):
             var_name = e.split(sep=".")[0]
@@ -106,7 +107,9 @@ class _Evaluator:
             for node in ast.walk(st):
                 if isinstance(node, ast.Name):
                     var_name = node.id.split(sep=".")[0]
-                    if var_name not in args and var_name not in targets and var_name not in non_vars:
+                    if var_name in builtin_vars:
+                        _warn(f"Variable '{var_name}' cannot be used in Taipy expressions as its name collides with a Python built-in identifier.")  # noqa: E501
+                    elif var_name not in args and var_name not in targets and var_name not in non_vars:
                         try:
                             if lazy_declare and var_name.startswith("__"):
                                 with warnings.catch_warnings(record=True) as warns:

+ 4 - 0
taipy/gui_core/_GuiCoreLib.py

@@ -219,6 +219,9 @@ class _GuiCore(ElementLibrary):
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "scenario": ElementProperty(PropertyType.lov_value, "optional"),
                 "width": ElementProperty(PropertyType.string),
+                "file_download": ElementProperty(PropertyType.boolean, False),
+                "file_upload": ElementProperty(PropertyType.boolean, False),
+                "upload_check": ElementProperty(PropertyType.function),
             },
             inner_properties={
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
@@ -259,6 +262,7 @@ class _GuiCore(ElementLibrary):
                     PropertyType.function, f"{{{__CTX_VAR_NAME}.tabular_data_edit}}"
                 ),
                 "on_lock": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.lock_datanode_for_edit}}"),
+                "on_file_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.on_file_action}}"),
                 "update_dn_vars": ElementProperty(
                     PropertyType.string,
                     f"data_id={__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>;"

+ 12 - 3
taipy/gui_core/_adapters.py

@@ -36,6 +36,7 @@ from taipy.core import (
 )
 from taipy.core import get as core_get
 from taipy.core.config import Config
+from taipy.core.data._file_datanode_mixin import _FileDataNodeMixin
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
 from taipy.core.reason import ReasonCollection
 from taipy.gui._warnings import _warn
@@ -59,6 +60,7 @@ class _EntityType(Enum):
 def _get_reason(rc: ReasonCollection, message: str):
     return "" if rc else f"{message}: {rc.reasons}"
 
+
 class _GuiCoreScenarioAdapter(_TaipyBase):
     __INNER_PROPS = ["name"]
 
@@ -225,11 +227,18 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
                         self.__get_data(datanode),
                         datanode._edit_in_progress,
                         datanode._editor_id,
-                        _get_reason(is_readable(datanode), "Datanode not readable"),
-                        _get_reason(is_editable(datanode), "Datanode not editable"),
+                        _get_reason(is_readable(datanode), "Data node not readable"),
+                        _get_reason(is_editable(datanode), "Data node not editable"),
+                        isinstance(datanode, _FileDataNodeMixin),
+                        f"Data unavailable: {reason.reasons}"
+                        if isinstance(datanode, _FileDataNodeMixin) and not (reason := datanode.is_downloadable())
+                        else "",
+                        f"Data unavailable: {reason.reasons}"
+                        if isinstance(datanode, _FileDataNodeMixin) and not (reason := datanode.is_uploadable())
+                        else "",
                     ]
             except Exception as e:
-                _warn(f"Access to datanode ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
+                _warn(f"Access to data node ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
 
         return None
 

+ 47 - 10
taipy/gui_core/_context.py

@@ -14,6 +14,7 @@ import json
 import typing as t
 from collections import defaultdict
 from numbers import Number
+from pathlib import Path
 from threading import Lock
 
 try:
@@ -53,6 +54,7 @@ from taipy.core import (
 from taipy.core import delete as core_delete
 from taipy.core import get as core_get
 from taipy.core import submit as core_submit
+from taipy.core.data._file_datanode_mixin import _FileDataNodeMixin
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
@@ -896,7 +898,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 self.__edit_properties(entity, data)
                 _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
-                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Data node. {e}")
 
     def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str]):
         self.__lazy_start()
@@ -906,7 +908,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         data = args[0]
         error_var = payload.get("error_id")
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(state, entity_id, "Datanode", error_var):
+        if not self.__check_readable_editable(state, entity_id, "Data node", error_var):
             return
         lock = data.get("lock", True)
         entity: DataNode = core_get(entity_id)
@@ -918,7 +920,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     entity.unlock_edit(self.gui._get_client_id())
                 _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
-                _GuiCoreContext.__assign_var(state, error_var, f"Error locking Datanode. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error locking Data node. {e}")
 
     def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: t.Dict[str, str]):
         with entity as ent:
@@ -1007,7 +1009,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         data = args[0]
         error_var = payload.get("error_id")
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(state, entity_id, "DataNode", error_var):
+        if not self.__check_readable_editable(state, entity_id, "Data node", error_var):
             return
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
@@ -1025,7 +1027,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 entity.unlock_edit(self.gui._get_client_id())
                 _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
-                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode value. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Data node value. {e}")
             _GuiCoreContext.__assign_var(state, payload.get("data_id"), entity_id)  # this will update the data value
 
     def tabular_data_edit(self, state: State, var_name: str, payload: dict):  # noqa:C901
@@ -1033,7 +1035,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         error_var = payload.get("error_id")
         user_data = payload.get("user_data", {})
         dn_id = user_data.get("dn_id")
-        if not self.__check_readable_editable(state, dn_id, "DataNode", error_var):
+        if not self.__check_readable_editable(state, dn_id, "Data node", error_var):
             return
         datanode = core_get(dn_id) if dn_id else None
         if isinstance(datanode, DataNode):
@@ -1070,7 +1072,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         _GuiCoreContext.__assign_var(
                             state,
                             error_var,
-                            "Error updating Datanode: dict values must be list or tuple.",
+                            "Error updating Data node: dict values must be list or tuple.",
                         )
                 else:
                     data_tuple = False
@@ -1095,7 +1097,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             _GuiCoreContext.__assign_var(
                                 state,
                                 error_var,
-                                "Error updating Datanode: cannot handle multi-column list value.",
+                                "Error updating data node: cannot handle multi-column list value.",
                             )
                         if data_tuple and new_data is not None:
                             new_data = tuple(new_data)
@@ -1103,13 +1105,13 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         _GuiCoreContext.__assign_var(
                             state,
                             error_var,
-                            "Error updating Datanode tabular value: type does not support at[] indexer.",
+                            "Error updating data node tabular value: type does not support at[] indexer.",
                         )
                 if new_data is not None:
                     datanode.write(new_data, comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT))
                     _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
-                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode tabular value. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating data node tabular value. {e}")
         _GuiCoreContext.__assign_var(state, payload.get("data_id"), dn_id)
 
     def get_data_node_properties(self, id: str):
@@ -1199,6 +1201,41 @@ class _GuiCoreContext(CoreEventConsumerBase):
         self.__lazy_start()
         return "" if (reason := can_create()) else f"Cannot create scenario: {_get_reason(reason)}"
 
+    def on_file_action(self, state: State, id: str, payload: t.Dict[str, t.Any]):
+        args = t.cast(list, payload.get("args"))
+        act_payload = t.cast(t.Dict[str, str], args[0])
+        dn_id = t.cast(DataNodeId, act_payload.get("id"))
+        error_id = act_payload.get("error_id", "")
+        if reason := is_readable(dn_id):
+            try:
+                dn = t.cast(_FileDataNodeMixin, core_get(dn_id))
+                if act_payload.get("action") == "export":
+                    path = dn._get_downloadable_path()
+                    if path:
+                        self.gui._download(Path(path), dn_id)
+                    else:
+                        reason = dn.is_downloadable()
+                        state.assign(
+                            error_id,
+                            "Data unavailable: "
+                            + ("The data node has never been written." if reason else reason.reasons),
+                        )
+                else:
+                    checker_name = act_payload.get("upload_check")
+                    checker = self.gui._get_user_function(checker_name) if checker_name else None
+                    if not (
+                        reason := dn._upload(
+                            act_payload.get("path", ""),
+                            t.cast(t.Callable[[str, t.Any], bool], checker) if callable(checker) else None,
+                        )
+                    ):
+                        state.assign(error_id, f"Data unavailable: {reason.reasons}")
+
+            except Exception as e:
+                state.assign(error_id, f"Data node download error: {e}")
+        else:
+            state.assign(error_id, reason.reasons)
+
 
 def _get_reason(reason: t.Union[bool, ReasonCollection]):
     return reason.reasons if isinstance(reason, ReasonCollection) else " "

+ 29 - 0
tests/core/data/test_csv_data_node.py

@@ -30,6 +30,7 @@ from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.csv import CSVDataNode
 from taipy.core.data.data_node_id import DataNodeId
 from taipy.core.exceptions.exceptions import InvalidExposedType
+from taipy.core.reason import NoFileToDownload, NotAFile
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -194,6 +195,29 @@ class TestCSVDataNode:
         assert ".data" not in dn.path
         assert os.path.exists(dn.path)
 
+    def test_is_downloadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.csv")
+        dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert reasons
+        assert reasons.reasons == ""
+
+    def test_is_not_downloadable_no_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/wrong_example.csv")
+        dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NoFileToDownload(path, dn.id)) in reasons.reasons
+
+    def test_is_not_downloadable_not_a_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample")
+        dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NotAFile(path, dn.id)) in reasons.reasons
+
     def test_get_downloadable_path(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.csv")
         dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
@@ -203,6 +227,11 @@ class TestCSVDataNode:
         dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": "NOT_EXISTING.csv", "exposed_type": "pandas"})
         assert dn._get_downloadable_path() == ""
 
+    def is_uploadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.csv")
+        dn = CSVDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        assert dn.is_uploadable()
+
     def test_upload(self, csv_file, tmpdir_factory):
         old_csv_path = tmpdir_factory.mktemp("data").join("df.csv").strpath
         old_data = pd.DataFrame([{"a": 0, "b": 1, "c": 2}, {"a": 3, "b": 4, "c": 5}])

+ 24 - 0
tests/core/data/test_excel_data_node.py

@@ -33,6 +33,7 @@ from taipy.core.exceptions.exceptions import (
     InvalidExposedType,
     NonExistingExcelSheet,
 )
+from taipy.core.reason import NoFileToDownload, NotAFile
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -409,6 +410,29 @@ class TestExcelDataNode:
         assert ".data" not in dn.path
         assert os.path.exists(dn.path)
 
+    def test_is_downloadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.xlsx")
+        dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert reasons
+        assert reasons.reasons == ""
+
+    def test_is_not_downloadable_no_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/wrong_path.xlsx")
+        dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NoFileToDownload(path, dn.id)) in reasons.reasons
+
+    def test_is_not_downloadable_not_a_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample")
+        dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NotAFile(path, dn.id)) in reasons.reasons
+
     def test_get_download_path(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.xlsx")
         dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})

+ 24 - 0
tests/core/data/test_json_data_node.py

@@ -32,6 +32,7 @@ from taipy.core.data.data_node_id import DataNodeId
 from taipy.core.data.json import JSONDataNode
 from taipy.core.data.operator import JoinOperator, Operator
 from taipy.core.exceptions.exceptions import NoData
+from taipy.core.reason import NoFileToDownload, NotAFile
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -391,6 +392,29 @@ class TestJSONDataNode:
         assert ".data" not in dn.path
         assert os.path.exists(dn.path)
 
+    def test_is_downloadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/json/example_dict.json")
+        dn = JSONDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert reasons
+        assert reasons.reasons == ""
+
+    def test_is_not_downloadable_no_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/json/wrong_path.json")
+        dn = JSONDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NoFileToDownload(path, dn.id)) in reasons.reasons
+
+    def is_not_downloadable_not_a_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/json")
+        dn = JSONDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NotAFile(path, dn.id)) in reasons.reasons
+
     def test_get_download_path(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/json/example_dict.json")
         dn = JSONDataNode("foo", Scope.SCENARIO, properties={"path": path})

+ 24 - 0
tests/core/data/test_parquet_data_node.py

@@ -34,6 +34,7 @@ from taipy.core.exceptions.exceptions import (
     UnknownCompressionAlgorithm,
     UnknownParquetEngine,
 )
+from taipy.core.reason import NoFileToDownload, NotAFile
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -234,6 +235,29 @@ class TestParquetDataNode:
         assert ".data" not in dn.path
         assert os.path.exists(dn.path)
 
+    def test_is_downloadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.parquet")
+        dn = ParquetDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert reasons
+        assert reasons.reasons == ""
+
+    def test_is_not_downloadable_no_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/wrong_path.parquet")
+        dn = ParquetDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NoFileToDownload(path, dn.id)) in reasons.reasons
+
+    def test_is_not_downloadable_not_a_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample")
+        dn = ParquetDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NotAFile(path, dn.id)) in reasons.reasons
+
     def test_get_downloadable_path(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.parquet")
         dn = ParquetDataNode("foo", Scope.SCENARIO, properties={"path": path, "exposed_type": "pandas"})

+ 25 - 0
tests/core/data/test_pickle_data_node.py

@@ -27,6 +27,7 @@ from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.exceptions.exceptions import NoData
+from taipy.core.reason import NoFileToDownload, NotAFile
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -205,6 +206,30 @@ class TestPickleDataNodeEntity:
         assert ".data" not in dn.path
         assert os.path.exists(dn.path)
 
+    def test_is_downloadable(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.p")
+        dn = PickleDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert reasons
+        assert reasons.reasons == ""
+
+    def test_is_not_downloadable_no_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/wrong_path.p")
+        dn = PickleDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NoFileToDownload(path, dn.id)) in reasons.reasons
+
+    def test_is_not_downloadable_not_a_file(self):
+        path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample")
+        dn = PickleDataNode("foo", Scope.SCENARIO, properties={"path": path})
+        reasons = dn.is_downloadable()
+        assert not reasons
+        assert len(reasons._reasons) == 1
+        assert str(NotAFile(path, dn.id)) in reasons.reasons
+
     def test_get_download_path(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.p")
         dn = PickleDataNode("foo", Scope.SCENARIO, properties={"path": path})

+ 2 - 1
tests/gui/control/test_date_range.py

@@ -45,7 +45,8 @@ def test_date_range_md_2(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
-def test_date_range_md_width(gui: Gui, helpers):
+def test_date_range_md_width(gui: Gui, test_client, helpers):
+    # do not remove test_client: it brings an app context needed for this test
     gui._bind_var_val(
         "dates", [datetime.strptime("15 Dec 2020", "%d %b %Y"), datetime.strptime("31 Dec 2020", "%d %b %Y")]
     )

+ 10 - 0
tests/gui/control/test_part.py

@@ -32,6 +32,16 @@ def test_part_md_2(gui: Gui, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_part_md_width(gui: Gui, helpers):
+    md_string = """
+<|part|width=70%|>
+# This is a part
+<|>
+"""
+    expected_list = ["<Part", 'width="70%"']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_part_html(gui: Gui, helpers):
     html_string = '<taipy:part class_name="class1"><h1>This is a part</h1></taipy:part>'
     expected_list = ["<Part", "<h1", "This is a part"]

+ 1 - 1
tests/gui/control/test_text.py

@@ -21,7 +21,7 @@ def test_text_md_1(gui: Gui, test_client, helpers):
 
 def test_text_md_width(gui: Gui, test_client, helpers):
     gui._bind_var_val("x", 10)
-    md_string = "<|{x}|width=70%|>"
+    md_string = "<|{x}|text|width=70%|>"
     expected_list = ["<Field", 'dataType="int"', 'defaultValue="10"', "value={tpec_TpExPr_x_TPMDL_0}", 'width="70%"']
     helpers.test_control_md(gui, md_string, expected_list)
 

+ 1 - 0
tests/gui/helpers.py

@@ -153,6 +153,7 @@ class Helpers:
                 run_in_thread=True,
                 allow_unsafe_werkzeug=False,
                 notebook_proxy=False,
+                port_auto_ranges=gui._get_config("port_auto_ranges", None),
             )
         while not Helpers.port_check():
             time.sleep(0.1)

+ 1 - 4
tests/gui_core/test_context_is_editable.py

@@ -282,10 +282,7 @@ class TestGuiCoreContext_is_editable:
             )
             assign.assert_called_once()
             assert assign.call_args_list[0].args[0] == "error_var"
-            assert (
-                assign.call_args_list[0].args[1]
-                == "Error updating Datanode tabular value: type does not support at[] indexer."
-            )
+            assert "tabular value: type does not support at[] indexer" in assign.call_args_list[0].args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):

+ 1 - 4
tests/gui_core/test_context_is_readable.py

@@ -420,10 +420,7 @@ class TestGuiCoreContext_is_readable:
             )
             assign.assert_called_once()
             assert assign.call_args_list[0].args[0] == "error_var"
-            assert (
-                assign.call_args_list[0].args[1]
-                == "Error updating Datanode tabular value: type does not support at[] indexer."
-            )
+            assert "tabular value: type does not support at[] indexer" in assign.call_args_list[0].args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):

Vissa filer visades inte eftersom för många filer har ändrats