Explorar o código

Merge branch 'develop' into bug/#746-scenario-submittable-not-clear-in-visual-element

Toan Quach hai 1 ano
pai
achega
8bffdadcaf
Modificáronse 55 ficheiros con 1641 adicións e 554 borrados
  1. 5 2
      README.md
  2. 15 0
      frontend/taipy-gui/packaging/taipy-gui.d.ts
  3. 11 4
      frontend/taipy-gui/src/components/Taipy/TableFilter.tsx
  4. 10 1
      frontend/taipy-gui/src/extensions/exports.ts
  5. 10 1
      frontend/taipy/src/CoreSelector.tsx
  6. 24 9
      frontend/taipy/src/DataNodeTable.tsx
  7. 15 11
      frontend/taipy/src/DataNodeViewer.tsx
  8. 7 3
      frontend/taipy/src/JobSelector.tsx
  9. 22 4
      frontend/taipy/src/PropertiesEditor.tsx
  10. 87 11
      frontend/taipy/src/ScenarioSelector.tsx
  11. 34 9
      frontend/taipy/src/ScenarioViewer.tsx
  12. 1 0
      pyproject.toml
  13. 7 0
      taipy/core/_manager/_manager.py
  14. 28 0
      taipy/core/_repository/_abstract_repository.py
  15. 15 0
      taipy/core/_version/_version_manager.py
  16. 3 2
      taipy/core/_version/_version_manager_factory.py
  17. 1 2
      taipy/core/cycle/_cycle_manager_factory.py
  18. 20 2
      taipy/core/data/_data_manager.py
  19. 1 2
      taipy/core/data/_data_manager_factory.py
  20. 4 4
      taipy/core/data/_filter.py
  21. 15 14
      taipy/core/data/data_node.py
  22. 35 8
      taipy/core/exceptions/exceptions.py
  23. 3 1
      taipy/core/job/_job_manager.py
  24. 1 2
      taipy/core/job/_job_manager_factory.py
  25. 92 3
      taipy/core/scenario/_scenario_manager.py
  26. 1 2
      taipy/core/scenario/_scenario_manager_factory.py
  27. 1 2
      taipy/core/submission/_submission_manager_factory.py
  28. 111 57
      taipy/core/taipy.py
  29. 1 2
      taipy/core/task/_task_manager_factory.py
  30. 5 2
      taipy/gui/_renderers/builder.py
  31. 1 1
      taipy/gui/data/utils.py
  32. 11 4
      taipy/gui/extension/library.py
  33. 3 3
      taipy/gui/gui.py
  34. 3 2
      taipy/gui/utils/_adapter.py
  35. 15 5
      taipy/gui/utils/_evaluator.py
  36. 3 2
      taipy/gui/utils/types.py
  37. 63 46
      taipy/gui_core/_GuiCoreLib.py
  38. 82 26
      taipy/gui_core/_adapters.py
  39. 155 103
      taipy/gui_core/_context.py
  40. 5 0
      taipy/gui_core/viselements.json
  41. 15 0
      tests/core/data/test_data_node.py
  42. 80 52
      tests/core/test_taipy/test_export.py
  43. 63 56
      tests/core/test_taipy/test_export_with_sql_repo.py
  44. 213 0
      tests/core/test_taipy/test_import.py
  45. 174 0
      tests/core/test_taipy/test_import_with_sql_repo.py
  46. 4 4
      tests/gui/utils/test_types.py
  47. 12 8
      tests/gui_core/test_context_is_deletable.py
  48. 39 25
      tests/gui_core/test_context_is_editable.py
  49. 6 4
      tests/gui_core/test_context_is_promotable.py
  50. 66 32
      tests/gui_core/test_context_is_readable.py
  51. 6 4
      tests/gui_core/test_context_is_submitable.py
  52. 16 0
      tests/rest/conftest.py
  53. 3 1
      tests/rest/test_end_to_end.py
  54. 7 8
      tests/rest/test_scenario.py
  55. 11 8
      tests/rest/test_sequence.py

+ 5 - 2
README.md

@@ -1,8 +1,11 @@
+[![Taipy Designer](https://github.com/nevo-david/taipy/assets/100117126/e787ba7b-ec7a-4d3f-a7e4-0f195daadce7)
+](https://taipy.io/enterprise)
+
 <div align="center">
 <div align="center">
   <a href="https://taipy.io?utm_source=github" target="_blank">
   <a href="https://taipy.io?utm_source=github" target="_blank">
   <picture>
   <picture>
-    <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Avaiga/taipy/assets/100117126/f59f70e9-1905-4abc-8760-8631b57c14c2">
-    <img alt="Taipy" src="readme_img/readme_logo.png" width="200" />
+    <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Avaiga/taipy/assets/100117126/509bf101-54c2-4321-adaf-a2af63af9682">
+    <img alt="Taipy" src="https://github.com/Avaiga/taipy/assets/100117126/4df8a733-d8d0-4893-acf0-d24ef9e8b58a" width="400" />
   </picture>
   </picture>
   </a>
   </a>
 </div>
 </div>

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

@@ -117,6 +117,21 @@ export interface TableProps extends TaipyPaginatedTableProps {
 }
 }
 export declare const Table: (props: TableProps) => JSX.Element;
 export declare const Table: (props: TableProps) => JSX.Element;
 
 
+export interface FilterDesc {
+    col: string;
+    action: string;
+    value: string | number | boolean | Date;
+}
+export interface TableFilterProps {
+    columns: Record<string, ColumnDesc>;
+    colsOrder?: Array<string>;
+    onValidate: (data: Array<FilterDesc>) => void;
+    appliedFilters?: Array<FilterDesc>;
+    className?: string;
+    filteredCount: number;
+}
+export declare const TableFilter: (props: TableFilterProps) => JSX.Element;
+
 export declare const Router: () => JSX.Element;
 export declare const Router: () => JSX.Element;
 
 
 /**
 /**

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

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
+import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
 import CheckIcon from "@mui/icons-material/Check";
 import CheckIcon from "@mui/icons-material/Check";
 import DeleteIcon from "@mui/icons-material/Delete";
 import DeleteIcon from "@mui/icons-material/Delete";
 import FilterListIcon from "@mui/icons-material/FilterList";
 import FilterListIcon from "@mui/icons-material/FilterList";
@@ -29,7 +29,7 @@ import Tooltip from "@mui/material/Tooltip";
 import { DateField, LocalizationProvider } from "@mui/x-date-pickers";
 import { DateField, LocalizationProvider } from "@mui/x-date-pickers";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 
 
-import { ColumnDesc, defaultDateFormat, iconInRowSx } from "./tableUtils";
+import { ColumnDesc, defaultDateFormat, getsortByIndex, iconInRowSx } from "./tableUtils";
 import { getDateTime, getTypeFromDf } from "../../utils";
 import { getDateTime, getTypeFromDf } from "../../utils";
 import { getSuffixedClassNames } from "./utils";
 import { getSuffixedClassNames } from "./utils";
 
 
@@ -41,7 +41,7 @@ export interface FilterDesc {
 
 
 interface TableFilterProps {
 interface TableFilterProps {
     columns: Record<string, ColumnDesc>;
     columns: Record<string, ColumnDesc>;
-    colsOrder: Array<string>;
+    colsOrder?: Array<string>;
     onValidate: (data: Array<FilterDesc>) => void;
     onValidate: (data: Array<FilterDesc>) => void;
     appliedFilters?: Array<FilterDesc>;
     appliedFilters?: Array<FilterDesc>;
     className?: string;
     className?: string;
@@ -279,12 +279,19 @@ const FilterRow = (props: FilterRowProps) => {
 };
 };
 
 
 const TableFilter = (props: TableFilterProps) => {
 const TableFilter = (props: TableFilterProps) => {
-    const { onValidate, appliedFilters, columns, colsOrder, className = "", filteredCount } = props;
+    const { onValidate, appliedFilters, columns, className = "", filteredCount } = props;
 
 
     const [showFilter, setShowFilter] = useState(false);
     const [showFilter, setShowFilter] = useState(false);
     const filterRef = useRef<HTMLButtonElement | null>(null);
     const filterRef = useRef<HTMLButtonElement | null>(null);
     const [filters, setFilters] = useState<Array<FilterDesc>>([]);
     const [filters, setFilters] = useState<Array<FilterDesc>>([]);
 
 
+    const colsOrder = useMemo(()=> {
+        if (props.colsOrder) {
+            return props.colsOrder;
+        }
+        return Object.keys(columns).sort(getsortByIndex(columns));
+    }, [props.colsOrder, columns]);
+
     const onShowFilterClick = useCallback(() => setShowFilter((f) => !f), []);
     const onShowFilterClick = useCallback(() => setShowFilter((f) => !f), []);
 
 
     const updateFilter = useCallback(
     const updateFilter = useCallback(

+ 10 - 1
frontend/taipy-gui/src/extensions/exports.ts

@@ -16,13 +16,20 @@ import Dialog from "../components/Taipy/Dialog";
 import Login from "../components/Taipy/Login";
 import Login from "../components/Taipy/Login";
 import Router from "../components/Router";
 import Router from "../components/Router";
 import Table from "../components/Taipy/Table";
 import Table from "../components/Taipy/Table";
+import TableFilter, { FilterDesc } from "../components/Taipy/TableFilter";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { LovItem } from "../utils/lov";
 import { LovItem } from "../utils/lov";
 import { getUpdateVar } from "../components/Taipy/utils";
 import { getUpdateVar } from "../components/Taipy/utils";
 import { ColumnDesc, RowType, RowValue } from "../components/Taipy/tableUtils";
 import { ColumnDesc, RowType, RowValue } from "../components/Taipy/tableUtils";
 import { TaipyContext, TaipyStore } from "../context/taipyContext";
 import { TaipyContext, TaipyStore } from "../context/taipyContext";
 import { TaipyBaseAction, TaipyState } from "../context/taipyReducers";
 import { TaipyBaseAction, TaipyState } from "../context/taipyReducers";
-import { useClassNames, useDispatchRequestUpdateOnFirstRender, useDispatch, useDynamicProperty, useModule } from "../utils/hooks";
+import {
+    useClassNames,
+    useDispatchRequestUpdateOnFirstRender,
+    useDispatch,
+    useDynamicProperty,
+    useModule,
+} from "../utils/hooks";
 import {
 import {
     createSendActionNameAction,
     createSendActionNameAction,
     createSendUpdateAction,
     createSendUpdateAction,
@@ -36,6 +43,7 @@ export {
     Login,
     Login,
     Router,
     Router,
     Table,
     Table,
+    TableFilter,
     TaipyContext as Context,
     TaipyContext as Context,
     createRequestDataUpdateAction,
     createRequestDataUpdateAction,
     createRequestUpdateAction,
     createRequestUpdateAction,
@@ -52,6 +60,7 @@ export {
 
 
 export type {
 export type {
     ColumnDesc,
     ColumnDesc,
+    FilterDesc,
     LoV,
     LoV,
     LoVElt,
     LoVElt,
     LovItem,
     LovItem,

+ 10 - 1
frontend/taipy/src/CoreSelector.tsx

@@ -31,6 +31,7 @@ import {
     createSendUpdateAction,
     createSendUpdateAction,
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     createRequestUpdateAction,
     createRequestUpdateAction,
+    useDynamicProperty,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
 import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence } from "./utils/types";
 import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence } from "./utils/types";
@@ -54,6 +55,7 @@ import {
 
 
 export interface EditProps {
 export interface EditProps {
     id: string;
     id: string;
+    active: boolean;
 }
 }
 
 
 const treeSlots = { expandIcon: ChevronRight };
 const treeSlots = { expandIcon: ChevronRight };
@@ -64,6 +66,8 @@ type Pinned = Record<string, boolean>;
 
 
 interface CoreSelectorProps {
 interface CoreSelectorProps {
     id?: string;
     id?: string;
+    active?: boolean;
+    defaultActive?: boolean;
     updateVarName?: string;
     updateVarName?: string;
     entities?: Entities;
     entities?: Entities;
     coreChanged?: Record<string, unknown>;
     coreChanged?: Record<string, unknown>;
@@ -109,6 +113,7 @@ const CoreItem = (props: {
     pins: [Pinned, Pinned];
     pins: [Pinned, Pinned];
     onPin?: (e: MouseEvent<HTMLElement>) => void;
     onPin?: (e: MouseEvent<HTMLElement>) => void;
     hideNonPinned: boolean;
     hideNonPinned: boolean;
+    active: boolean;
 }) => {
 }) => {
     const [id, label, items = EmptyArray, nodeType, primary] = props.item;
     const [id, label, items = EmptyArray, nodeType, primary] = props.item;
     const isPinned = props.pins[0][id];
     const isPinned = props.pins[0][id];
@@ -126,6 +131,7 @@ const CoreItem = (props: {
                     pins={props.pins}
                     pins={props.pins}
                     onPin={props.onPin}
                     onPin={props.onPin}
                     hideNonPinned={props.hideNonPinned}
                     hideNonPinned={props.hideNonPinned}
+                    active={props.active}
                 />
                 />
             ))}
             ))}
         </>
         </>
@@ -161,7 +167,7 @@ const CoreItem = (props: {
                     </Grid>
                     </Grid>
                     {props.editComponent && nodeType === props.leafType ? (
                     {props.editComponent && nodeType === props.leafType ? (
                         <Grid item xs="auto">
                         <Grid item xs="auto">
-                            <props.editComponent id={id} />
+                            <props.editComponent id={id} active={props.active} />
                         </Grid>
                         </Grid>
                     ) : null}
                     ) : null}
                     {props.onPin ? (
                     {props.onPin ? (
@@ -194,6 +200,7 @@ const CoreItem = (props: {
                     pins={props.pins}
                     pins={props.pins}
                     onPin={props.onPin}
                     onPin={props.onPin}
                     hideNonPinned={props.hideNonPinned}
                     hideNonPinned={props.hideNonPinned}
+                    active={props.active}
                 />
                 />
             ))}
             ))}
         </TreeItem>
         </TreeItem>
@@ -261,6 +268,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
     const [hideNonPinned, setShowPinned] = useState(false);
     const [hideNonPinned, setShowPinned] = useState(false);
     const [expandedItems, setExpandedItems] = useState<string[]>([]);
     const [expandedItems, setExpandedItems] = useState<string[]>([]);
 
 
+    const active = useDynamicProperty(props.active, props.defaultActive, true);
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
 
 
@@ -437,6 +445,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                               onPin={showPins ? onPin : undefined}
                               onPin={showPins ? onPin : undefined}
                               pins={pins}
                               pins={pins}
                               hideNonPinned={hideNonPinned}
                               hideNonPinned={hideNonPinned}
+                              active={!!active}
                           />
                           />
                       ))
                       ))
                     : null}
                     : null}

+ 24 - 9
frontend/taipy/src/DataNodeTable.tsx

@@ -33,7 +33,15 @@ import TextField from "@mui/material/TextField";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 
 
-import { ColumnDesc, Table, TraceValueType, createSendActionNameAction, useDispatch, useModule } from "taipy-gui";
+import {
+    ColumnDesc,
+    Table,
+    TraceValueType,
+    createSendActionNameAction,
+    getUpdateVar,
+    useDispatch,
+    useModule,
+} from "taipy-gui";
 
 
 import { ChartViewType, MenuProps, TableViewType, selectSx, tabularHeaderSx } from "./utils";
 import { ChartViewType, MenuProps, TableViewType, selectSx, tabularHeaderSx } from "./utils";
 
 
@@ -51,13 +59,13 @@ interface DataNodeTableProps {
     editInProgress?: boolean;
     editInProgress?: boolean;
     editLock: MutableRefObject<boolean>;
     editLock: MutableRefObject<boolean>;
     editable: boolean;
     editable: boolean;
-    idVar?: string;
+    updateDnVars?: string;
 }
 }
 
 
 const pushRightSx = { ml: "auto" };
 const pushRightSx = { ml: "auto" };
 
 
 const DataNodeTable = (props: DataNodeTableProps) => {
 const DataNodeTable = (props: DataNodeTableProps) => {
-    const { uniqid, configId, nodeId, columns = "", onViewTypeChange, editable } = props;
+    const { uniqid, configId, nodeId, columns = "", onViewTypeChange, editable, updateDnVars = "" } = props;
 
 
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
@@ -112,17 +120,24 @@ const DataNodeTable = (props: DataNodeTableProps) => {
         () =>
         () =>
             setTableEdit((e) => {
             setTableEdit((e) => {
                 props.editLock.current = !e;
                 props.editLock.current = !e;
-                dispatch(createSendActionNameAction("", module, props.onLock, { id: nodeId, lock: !e }));
+                dispatch(
+                    createSendActionNameAction("", module, props.onLock, {
+                        id: nodeId,
+                        lock: !e,
+                        error_id: getUpdateVar(updateDnVars, "error_id"),
+                    })
+                );
                 return !e;
                 return !e;
             }),
             }),
-        [nodeId, dispatch, module, props.onLock, props.editLock]
+        [nodeId, dispatch, module, props.onLock, props.editLock, updateDnVars]
     );
     );
 
 
     const userData = useMemo(() => {
     const userData = useMemo(() => {
-        const ret: Record<string, unknown> = {dn_id: nodeId, comment: ""};
-        props.idVar && (ret.context = { [props.idVar]: nodeId });
-        return ret
-    }, [nodeId, props.idVar]);
+        const ret: Record<string, unknown> = { dn_id: nodeId, comment: "" };
+        const idVar = getUpdateVar(updateDnVars, "data_id");
+        idVar && (ret.context = { [idVar]: nodeId, data_id: idVar, error_id: getUpdateVar(updateDnVars, "error_id") });
+        return ret;
+    }, [nodeId, updateDnVars]);
     const [comment, setComment] = useState("");
     const [comment, setComment] = useState("");
     const changeComment = useCallback(
     const changeComment = useCallback(
         (e: ChangeEvent<HTMLInputElement>) => {
         (e: ChangeEvent<HTMLInputElement>) => {

+ 15 - 11
frontend/taipy/src/DataNodeViewer.tsx

@@ -310,7 +310,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                 createRequestUpdateAction(
                                 createRequestUpdateAction(
                                     id,
                                     id,
                                     module,
                                     module,
-                                    getUpdateVarNames(updateVars, "properties"),
+                                    getUpdateVarNames(updateVars, "dnProperties"),
                                     true,
                                     true,
                                     idVar ? { [idVar]: dnId } : undefined
                                     idVar ? { [idVar]: dnId } : undefined
                                 )
                                 )
@@ -354,6 +354,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                             createSendActionNameAction(id, module, props.onLock, {
                             createSendActionNameAction(id, module, props.onLock, {
                                 id: oldId,
                                 id: oldId,
                                 lock: false,
                                 lock: false,
+                                error_id: getUpdateVar(updateDnVars, "error_id")
                             })
                             })
                         ),
                         ),
                     1
                     1
@@ -442,9 +443,9 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         () => () => {
         () => () => {
             dnId &&
             dnId &&
                 editLock.current &&
                 editLock.current &&
-                dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: false }));
+                dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: false, error_id: getUpdateVar(updateDnVars, "error_id") }));
         },
         },
-        [dnId, id, dispatch, module, props.onLock]
+        [dnId, id, dispatch, module, props.onLock, updateDnVars]
     );
     );
 
 
     const active = useDynamicProperty(props.active, props.defaultActive, true) && dnReadable;
     const active = useDynamicProperty(props.active, props.defaultActive, true) && dnReadable;
@@ -469,11 +470,11 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
             e.stopPropagation();
             e.stopPropagation();
             setFocusName(e.currentTarget.dataset.focus || "");
             setFocusName(e.currentTarget.dataset.focus || "");
             if (e.currentTarget.dataset.focus === dataValueFocus && !editLock.current) {
             if (e.currentTarget.dataset.focus === dataValueFocus && !editLock.current) {
-                dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: true }));
+                dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: true, error_id: getUpdateVar(updateDnVars, "error_id") }));
                 editLock.current = true;
                 editLock.current = true;
             }
             }
         },
         },
-        [dnId, props.onLock, id, dispatch, module]
+        [dnId, props.onLock, id, dispatch, module, updateDnVars]
     );
     );
 
 
     // Label
     // Label
@@ -482,11 +483,11 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         (e: MouseEvent<HTMLElement>) => {
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             e.stopPropagation();
             if (valid) {
             if (valid) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: dnId, name: label }));
+                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: dnId, name: label, error_id: getUpdateVar(updateDnVars, "error_id") }));
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [valid, props.onEdit, dnId, label, id, dispatch, module]
+        [valid, props.onEdit, dnId, label, id, dispatch, module, updateDnVars]
     );
     );
     const cancelLabel = useCallback(
     const cancelLabel = useCallback(
         (e: MouseEvent<HTMLElement>) => {
         (e: MouseEvent<HTMLElement>) => {
@@ -548,21 +549,23 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                         value: dataValue,
                         value: dataValue,
                         type: dtType,
                         type: dtType,
                         comment: comment,
                         comment: comment,
+                        error_id: getUpdateVar(updateDnVars, "error_id"),
+                        data_id: getUpdateVar(updateDnVars, "data_id")
                     })
                     })
                 );
                 );
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [valid, props.onDataValue, dnId, dataValue, dtType, id, dispatch, module, comment]
+        [valid, props.onDataValue, dnId, dataValue, dtType, id, dispatch, module, comment, updateDnVars]
     );
     );
     const cancelDataValue = useCallback(
     const cancelDataValue = useCallback(
         (e: MouseEvent<HTMLElement>) => {
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             e.stopPropagation();
             setDataValue(getDataValue(dtValue, dtType));
             setDataValue(getDataValue(dtValue, dtType));
             setFocusName("");
             setFocusName("");
-            dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: false }));
+            dispatch(createSendActionNameAction(id, module, props.onLock, { id: dnId, lock: false, error_id: getUpdateVar(updateDnVars, "error_id") }));
         },
         },
-        [dtValue, dtType, dnId, id, dispatch, module, props.onLock]
+        [dtValue, dtType, dnId, id, dispatch, module, props.onLock, updateDnVars]
     );
     );
     const onDataValueChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setDataValue(e.target.value), []);
     const onDataValueChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setDataValue(e.target.value), []);
     const onDataValueDateChange = useCallback((d: Date | null) => d && setDataValue(d), []);
     const onDataValueDateChange = useCallback((d: Date | null) => d && setDataValue(d), []);
@@ -831,6 +834,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                     onFocus={onFocus}
                                     onFocus={onFocus}
                                     onEdit={props.onEdit}
                                     onEdit={props.onEdit}
                                     editable={dnEditable}
                                     editable={dnEditable}
+                                    updatePropVars={updateDnVars}
                                 />
                                 />
                             </Grid>
                             </Grid>
                         </div>
                         </div>
@@ -1059,7 +1063,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                 editInProgress={dnEditInProgress && dnEditorId !== editorId}
                                                 editInProgress={dnEditInProgress && dnEditorId !== editorId}
                                                 editLock={editLock}
                                                 editLock={editLock}
                                                 editable={dnEditable}
                                                 editable={dnEditable}
-                                                idVar={getUpdateVar(updateDnVars, "data_id")}
+                                                updateDnVars={updateDnVars}
                                             />
                                             />
                                         ) : (
                                         ) : (
                                             <DataNodeChart
                                             <DataNodeChart

+ 7 - 3
frontend/taipy/src/JobSelector.tsx

@@ -74,6 +74,7 @@ interface JobSelectorProps {
     value?: string;
     value?: string;
     defaultValue?: string;
     defaultValue?: string;
     propagate?: boolean;
     propagate?: boolean;
+    updateJbVars?: string;
 }
 }
 
 
 // job id, job name, empty list, entity id, entity name, submit id, creation date, status
 // job id, job name, empty list, entity id, entity name, submit id, creation date, status
@@ -406,7 +407,7 @@ const JobSelectedTableRow = ({
     showSubmissionId,
     showSubmissionId,
     showDate,
     showDate,
     showCancel,
     showCancel,
-    showDelete,
+    showDelete
 }: JobSelectedTableRowProps) => {
 }: JobSelectedTableRowProps) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const [id, jobName, _, entityId, entityName, submitId, creationDate, status] = row;
     const [id, jobName, _, entityId, entityName, submitId, creationDate, status] = row;
@@ -481,6 +482,7 @@ const JobSelector = (props: JobSelectorProps) => {
         showCancel = true,
         showCancel = true,
         showDelete = true,
         showDelete = true,
         propagate = true,
         propagate = true,
+        updateJbVars = ""
     } = props;
     } = props;
     const [checked, setChecked] = useState<string[]>([]);
     const [checked, setChecked] = useState<string[]>([]);
     const [selected, setSelected] = useState<string[]>([]);
     const [selected, setSelected] = useState<string[]>([]);
@@ -611,13 +613,14 @@ const JobSelector = (props: JobSelectorProps) => {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                         id: multiple === false ? [id] : JSON.parse(id),
                         id: multiple === false ? [id] : JSON.parse(id),
                         action: "cancel",
                         action: "cancel",
+                        error_id: getUpdateVar(updateJbVars, "error_id")
                     })
                     })
                 );
                 );
             } catch (e) {
             } catch (e) {
                 console.warn("Error parsing ids for cancel.", e);
                 console.warn("Error parsing ids for cancel.", e);
             }
             }
         },
         },
-        [dispatch, module, props.id, props.onJobAction]
+        [dispatch, module, props.id, props.onJobAction, updateJbVars]
     );
     );
 
 
     const handleDeleteJobs = useCallback(
     const handleDeleteJobs = useCallback(
@@ -629,13 +632,14 @@ const JobSelector = (props: JobSelectorProps) => {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                         id: multiple === false ? [id] : JSON.parse(id),
                         id: multiple === false ? [id] : JSON.parse(id),
                         action: "delete",
                         action: "delete",
+                        error_id: getUpdateVar(updateJbVars, "error_id")
                     })
                     })
                 );
                 );
             } catch (e) {
             } catch (e) {
                 console.warn("Error parsing ids for delete.", e);
                 console.warn("Error parsing ids for delete.", e);
             }
             }
         },
         },
-        [dispatch, module, props.id, props.onJobAction]
+        [dispatch, module, props.id, props.onJobAction, updateJbVars]
     );
     );
 
 
     const allowCancelJobs = useMemo(
     const allowCancelJobs = useMemo(

+ 22 - 4
frontend/taipy/src/PropertiesEditor.tsx

@@ -20,7 +20,7 @@ import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
 import Typography from "@mui/material/Typography";
 import { DeleteOutline, CheckCircle, Cancel } from "@mui/icons-material";
 import { DeleteOutline, CheckCircle, Cancel } from "@mui/icons-material";
 
 
-import { createSendActionNameAction, useDispatch, useModule } from "taipy-gui";
+import { createSendActionNameAction, getUpdateVar, useDispatch, useModule } from "taipy-gui";
 
 
 import { DeleteIconSx, FieldNoMaxWidth, IconPaddingSx, disableColor, hoverSx } from "./utils";
 import { DeleteIconSx, FieldNoMaxWidth, IconPaddingSx, disableColor, hoverSx } from "./utils";
 
 
@@ -34,6 +34,7 @@ type PropertiesEditPayload = {
     id: string;
     id: string;
     properties?: Property[];
     properties?: Property[];
     deleted_properties?: Array<Partial<Property>>;
     deleted_properties?: Array<Partial<Property>>;
+    error_id?: string;
 };
 };
 
 
 export type DatanodeProperties = Array<[string, string]>;
 export type DatanodeProperties = Array<[string, string]>;
@@ -50,10 +51,23 @@ interface PropertiesEditorProps {
     isDefined: boolean;
     isDefined: boolean;
     onEdit?: string;
     onEdit?: string;
     editable: boolean;
     editable: boolean;
+    updatePropVars?: string;
 }
 }
 
 
 const PropertiesEditor = (props: PropertiesEditorProps) => {
 const PropertiesEditor = (props: PropertiesEditorProps) => {
-    const { id, entityId, isDefined, show, active, onFocus, focusName, setFocusName, entProperties, editable } = props;
+    const {
+        id,
+        entityId,
+        isDefined,
+        show,
+        active,
+        onFocus,
+        focusName,
+        setFocusName,
+        entProperties,
+        editable,
+        updatePropVars = "",
+    } = props;
 
 
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
@@ -85,7 +99,11 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                 const property = propId ? properties.find((p) => p.id === propId) : newProp;
                 const property = propId ? properties.find((p) => p.id === propId) : newProp;
                 if (property) {
                 if (property) {
                     const oldId = property.id;
                     const oldId = property.id;
-                    const payload: PropertiesEditPayload = { id: entityId, properties: [property] };
+                    const payload: PropertiesEditPayload = {
+                        id: entityId,
+                        properties: [property],
+                        error_id: getUpdateVar(updatePropVars, "error_id"),
+                    };
                     if (oldId && oldId != property.key) {
                     if (oldId && oldId != property.key) {
                         payload.deleted_properties = [{ key: oldId }];
                         payload.deleted_properties = [{ key: oldId }];
                     }
                     }
@@ -95,7 +113,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [isDefined, props.onEdit, entityId, properties, newProp, id, dispatch, module, setFocusName]
+        [isDefined, props.onEdit, entityId, properties, newProp, id, dispatch, module, setFocusName, updatePropVars]
     );
     );
     const cancelProperty = useCallback(
     const cancelProperty = useCallback(
         (e?: MouseEvent<HTMLElement>, dataset?: DOMStringMap) => {
         (e?: MouseEvent<HTMLElement>, dataset?: DOMStringMap) => {

+ 87 - 11
frontend/taipy/src/ScenarioSelector.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useEffect, useState, useCallback } from "react";
+import React, { useEffect, useState, useCallback, useMemo } from "react";
 import { Theme, Tooltip, alpha } from "@mui/material";
 import { Theme, Tooltip, alpha } from "@mui/material";
 
 
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
@@ -35,10 +35,21 @@ import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 import { useFormik } from "formik";
 import { useFormik } from "formik";
 
 
-import { useDispatch, useModule, createSendActionNameAction, getUpdateVar, createSendUpdateAction } from "taipy-gui";
+import {
+    useDispatch,
+    useModule,
+    createSendActionNameAction,
+    getUpdateVar,
+    createSendUpdateAction,
+    TableFilter,
+    ColumnDesc,
+    FilterDesc,
+    useDynamicProperty,
+    createRequestUpdateAction,
+} from "taipy-gui";
 
 
 import ConfirmDialog from "./utils/ConfirmDialog";
 import ConfirmDialog from "./utils/ConfirmDialog";
-import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx } from "./utils";
+import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx, getUpdateVarNames } from "./utils";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 
 
@@ -62,6 +73,8 @@ interface ScenarioDict {
 
 
 interface ScenarioSelectorProps {
 interface ScenarioSelectorProps {
     id?: string;
     id?: string;
+    active?: boolean;
+    defaultActive?: boolean;
     showAddButton?: boolean;
     showAddButton?: boolean;
     displayCycles?: boolean;
     displayCycles?: boolean;
     showPrimaryFlag?: boolean;
     showPrimaryFlag?: boolean;
@@ -86,6 +99,8 @@ interface ScenarioSelectorProps {
     showPins?: boolean;
     showPins?: boolean;
     showDialog?: boolean;
     showDialog?: boolean;
     multiple?: boolean;
     multiple?: boolean;
+    filterBy?: string;
+    updateScVars?: string;
 }
 }
 
 
 interface ScenarioEditDialogProps {
 interface ScenarioEditDialogProps {
@@ -414,22 +429,68 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
 };
 };
 
 
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
-    const { showAddButton = true, propagate = true, showPins = false, showDialog = true, multiple = false } = props;
+    const {
+        showAddButton = true,
+        propagate = true,
+        showPins = false,
+        showDialog = true,
+        multiple = false,
+        updateVars = "",
+        updateScVars = "",
+    } = props;
     const [open, setOpen] = useState(false);
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
 
 
+    const active = useDynamicProperty(props.active, props.defaultActive, true);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
 
 
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
 
 
+    const colFilters = useMemo(() => {
+        try {
+            const res = props.filterBy ? (JSON.parse(props.filterBy) as Array<[string, string]>) : undefined;
+            return Array.isArray(res)
+                ? res.reduce((pv, [name, coltype], idx) => {
+                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true };
+                      return pv;
+                  }, {} as Record<string, ColumnDesc>)
+                : undefined;
+        } catch (e) {
+            return undefined;
+        }
+    }, [props.filterBy]);
+    const [filters, setFilters] = useState<FilterDesc[]>([]);
+
+    const applyFilters = useCallback(
+        (filters: FilterDesc[]) => {
+            setFilters((old) => {
+                if (old.length != filters.length || JSON.stringify(old) != JSON.stringify(filters)) {
+                    const filterVar = getUpdateVar(updateScVars, "filter");
+                    dispatch(
+                        createRequestUpdateAction(
+                            props.id,
+                            module,
+                            getUpdateVarNames(updateVars, "innerScenarios"),
+                            true,
+                            filterVar ? { [filterVar]: filters } : undefined
+                        )
+                    );
+                    return filters;
+                }
+                return old;
+            });
+        },
+        [updateVars, dispatch, props.id, updateScVars, module]
+    );
+
     const onSubmit = useCallback(
     const onSubmit = useCallback(
         (...values: unknown[]) => {
         (...values: unknown[]) => {
             dispatch(
             dispatch(
                 createSendActionNameAction(
                 createSendActionNameAction(
                     props.id,
                     props.id,
                     module,
                     module,
-                    props.onScenarioCrud,
+                    { action: props.onScenarioCrud, error_id: getUpdateVar(updateScVars, "error_id") },
                     props.onCreation,
                     props.onCreation,
                     props.updateVarName,
                     props.updateVarName,
                     ...values
                     ...values
@@ -437,7 +498,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             );
             );
             if (values.length > 1 && values[1]) {
             if (values.length > 1 && values[1]) {
                 // delete requested => unselect current node
                 // delete requested => unselect current node
-                const lovVar = getUpdateVar(props.updateVars, "innerScenarios");
+                const lovVar = getUpdateVar(updateVars, "innerScenarios");
                 dispatch(
                 dispatch(
                     createSendUpdateAction(props.updateVarName, undefined, module, props.onChange, propagate, lovVar)
                     createSendUpdateAction(props.updateVarName, undefined, module, props.onChange, propagate, lovVar)
                 );
                 );
@@ -451,8 +512,9 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             propagate,
             propagate,
             props.onChange,
             props.onChange,
             props.updateVarName,
             props.updateVarName,
-            props.updateVars,
+            updateVars,
             props.onCreation,
             props.onCreation,
+            updateScVars,
         ]
         ]
     );
     );
 
 
@@ -474,19 +536,25 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
         (e: React.MouseEvent<HTMLElement>) => {
         (e: React.MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             e.stopPropagation();
             const { id: scenId } = e.currentTarget?.dataset || {};
             const { id: scenId } = e.currentTarget?.dataset || {};
+            const varName = getUpdateVar(updateScVars, "sc_id");
             scenId &&
             scenId &&
                 props.onScenarioSelect &&
                 props.onScenarioSelect &&
-                dispatch(createSendActionNameAction(props.id, module, props.onScenarioSelect, scenId));
+                dispatch(createSendActionNameAction(props.id, module, props.onScenarioSelect, varName, scenId));
             setOpen(true);
             setOpen(true);
             setActionEdit(true);
             setActionEdit(true);
         },
         },
-        [props.onScenarioSelect, props.id, dispatch, module]
+        [props.onScenarioSelect, props.id, dispatch, module, updateScVars]
     );
     );
 
 
     const EditScenario = useCallback(
     const EditScenario = useCallback(
         (props: EditProps) => (
         (props: EditProps) => (
             <Tooltip title="Edit Scenario">
             <Tooltip title="Edit Scenario">
-                <IconButton data-id={props.id} onClick={openEditDialog} sx={tinyEditIconButtonSx}>
+                <IconButton
+                    data-id={props.id}
+                    onClick={openEditDialog}
+                    sx={tinyEditIconButtonSx}
+                    disabled={props.active}
+                >
                     <EditOutlined />
                     <EditOutlined />
                 </IconButton>
                 </IconButton>
             </Tooltip>
             </Tooltip>
@@ -497,6 +565,14 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     return (
     return (
         <>
         <>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
+                {active && colFilters ? (
+                    <TableFilter
+                        columns={colFilters}
+                        appliedFilters={filters}
+                        filteredCount={0}
+                        onValidate={applyFilters}
+                    ></TableFilter>
+                ) : null}
                 <CoreSelector
                 <CoreSelector
                     {...props}
                     {...props}
                     entities={props.innerScenarios}
                     entities={props.innerScenarios}
@@ -507,7 +583,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     multiple={multiple}
                     multiple={multiple}
                 />
                 />
                 {showAddButton ? (
                 {showAddButton ? (
-                    <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />}>
+                    <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />} disabled={!active}>
                         Add scenario
                         Add scenario
                     </Button>
                     </Button>
                 ) : null}
                 ) : null}

+ 34 - 9
frontend/taipy/src/ScenarioViewer.tsx

@@ -38,6 +38,7 @@ import deepEqual from "fast-deep-equal/es6";
 import {
 import {
     createRequestUpdateAction,
     createRequestUpdateAction,
     createSendActionNameAction,
     createSendActionNameAction,
+    getUpdateVar,
     useDispatch,
     useDispatch,
     useDynamicProperty,
     useDynamicProperty,
     useModule,
     useModule,
@@ -87,6 +88,7 @@ interface ScenarioViewerProps {
     className?: string;
     className?: string;
     dynamicClassName?: string;
     dynamicClassName?: string;
     onSubmissionChange?: string;
     onSubmissionChange?: string;
+    updateScVar?: string;
 }
 }
 
 
 interface SequencesRowProps {
 interface SequencesRowProps {
@@ -340,6 +342,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         showSubmit = true,
         showSubmit = true,
         showSubmitSequences = true,
         showSubmitSequences = true,
         showTags = true,
         showTags = true,
+        updateScVar = "",
     } = props;
     } = props;
 
 
     const dispatch = useDispatch();
     const dispatch = useDispatch();
@@ -401,9 +404,15 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const onPromote = useCallback(() => {
     const onPromote = useCallback(() => {
         setPrimaryDialog(false);
         setPrimaryDialog(false);
         if (valid) {
         if (valid) {
-            dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, primary: true }));
+            dispatch(
+                createSendActionNameAction(id, module, props.onEdit, {
+                    id: scId,
+                    primary: true,
+                    error_id: getUpdateVar(updateScVar, "error_id"),
+                })
+            );
         }
         }
-    }, [valid, props.onEdit, scId, id, dispatch, module]);
+    }, [valid, props.onEdit, scId, id, dispatch, module, updateScVar]);
 
 
     // userExpanded
     // userExpanded
     const [userExpanded, setUserExpanded] = useState(valid && expanded);
     const [userExpanded, setUserExpanded] = useState(valid && expanded);
@@ -421,10 +430,11 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                         id: scId,
                         id: scId,
                         sequence: label,
                         sequence: label,
                         on_submission_change: props.onSubmissionChange,
                         on_submission_change: props.onSubmissionChange,
+                        error_id: getUpdateVar(updateScVar, "error_id")
                     })
                     })
                 );
                 );
         },
         },
-        [scId, props.onSubmit, props.onSubmissionChange, id, dispatch, module]
+        [scId, props.onSubmit, props.onSubmissionChange, id, dispatch, module, updateScVar]
     );
     );
 
 
     const submitScenario = useCallback(
     const submitScenario = useCallback(
@@ -435,11 +445,12 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                     createSendActionNameAction(id, module, props.onSubmit, {
                     createSendActionNameAction(id, module, props.onSubmit, {
                         id: scId,
                         id: scId,
                         on_submission_change: props.onSubmissionChange,
                         on_submission_change: props.onSubmissionChange,
+                        error_id: getUpdateVar(updateScVar, "error_id")
                     })
                     })
                 );
                 );
             }
             }
         },
         },
-        [valid, props.onSubmit, props.onSubmissionChange, id, scId, dispatch, module]
+        [valid, props.onSubmit, props.onSubmissionChange, id, scId, dispatch, module, updateScVar]
     );
     );
 
 
     // focus
     // focus
@@ -455,11 +466,17 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         (e?: MouseEvent<HTMLElement>) => {
         (e?: MouseEvent<HTMLElement>) => {
             e && e.stopPropagation();
             e && e.stopPropagation();
             if (valid) {
             if (valid) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, name: label }));
+                dispatch(
+                    createSendActionNameAction(id, module, props.onEdit, {
+                        id: scId,
+                        name: label,
+                        error_id: getUpdateVar(updateScVar, "error_id"),
+                    })
+                );
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [valid, props.onEdit, scId, label, id, dispatch, module]
+        [valid, props.onEdit, scId, label, id, dispatch, module, updateScVar]
     );
     );
     const cancelLabel = useCallback(
     const cancelLabel = useCallback(
         (e?: MouseEvent<HTMLElement>) => {
         (e?: MouseEvent<HTMLElement>) => {
@@ -493,11 +510,17 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         (e?: MouseEvent<HTMLElement>) => {
         (e?: MouseEvent<HTMLElement>) => {
             e && e.stopPropagation();
             e && e.stopPropagation();
             if (valid) {
             if (valid) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, tags: tags }));
+                dispatch(
+                    createSendActionNameAction(id, module, props.onEdit, {
+                        id: scId,
+                        tags: tags,
+                        error_id: getUpdateVar(updateScVar, "error_id"),
+                    })
+                );
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [valid, props.onEdit, scId, tags, id, dispatch, module]
+        [valid, props.onEdit, scId, tags, id, dispatch, module, updateScVar]
     );
     );
     const cancelTags = useCallback(
     const cancelTags = useCallback(
         (e?: MouseEvent<HTMLElement>) => {
         (e?: MouseEvent<HTMLElement>) => {
@@ -532,6 +555,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                             name: label,
                             name: label,
                             task_ids: taskIds,
                             task_ids: taskIds,
                             del: !!del,
                             del: !!del,
+                            error_id: getUpdateVar(updateScVar, "error_id"),
                         })
                         })
                     );
                     );
                 } else {
                 } else {
@@ -540,7 +564,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                 setFocusName("");
                 setFocusName("");
             }
             }
         },
         },
-        [valid, id, scId, props.onEdit, dispatch, module]
+        [valid, id, scId, props.onEdit, dispatch, module, updateScVar]
     );
     );
     const isValidSequence = useCallback(
     const isValidSequence = useCallback(
         (sLabel: string, label: string) => !!label && (sLabel == label || !sequences.find((seq) => seq[0] === label)),
         (sLabel: string, label: string) => !!label && (sLabel == label || !sequences.find((seq) => seq[0] === label)),
@@ -800,6 +824,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 onFocus={onFocus}
                                 onFocus={onFocus}
                                 onEdit={props.onEdit}
                                 onEdit={props.onEdit}
                                 editable={scEditable}
                                 editable={scEditable}
+                                updatePropVars={updateScVar}
                             />
                             />
                             {showSequences ? (
                             {showSequences ? (
                                 <>
                                 <>

+ 1 - 0
pyproject.toml

@@ -37,6 +37,7 @@ unfixable = []
 "_init.py" = ["F401", "F403"]  # unused import
 "_init.py" = ["F401", "F403"]  # unused import
 "taipy/config/stubs/pyi_header.py" = ["F401", "F403"]  # unused import
 "taipy/config/stubs/pyi_header.py" = ["F401", "F403"]  # unused import
 "taipy/templates/*" = ["F401", "F403", "T201"]  # unused import, `print` found
 "taipy/templates/*" = ["F401", "F403", "T201"]  # unused import, `print` found
+"taipy/gui/utils/types.py" = ["B024"] # abstract base class with no abstract methods
 
 
 [tool.ruff.lint.mccabe]
 [tool.ruff.lint.mccabe]
 max-complexity = 18
 max-complexity = 18

+ 7 - 0
taipy/core/_manager/_manager.py

@@ -157,6 +157,13 @@ class _Manager(Generic[EntityType]):
     def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs):
     def _export(cls, id: str, folder_path: Union[str, pathlib.Path], **kwargs):
         return cls._repository._export(id, folder_path)
         return cls._repository._export(id, folder_path)
 
 
+    @classmethod
+    def _import(cls, entity_file: pathlib.Path, version: str, **kwargs) -> EntityType:
+        imported_entity = cls._repository._import(entity_file)
+        imported_entity._version = version
+        cls._set(imported_entity)
+        return imported_entity
+
     @classmethod
     @classmethod
     def _is_editable(cls, entity: Union[EntityType, str]) -> bool:
     def _is_editable(cls, entity: Union[EntityType, str]) -> bool:
         return True
         return True

+ 28 - 0
taipy/core/_repository/_abstract_repository.py

@@ -9,10 +9,14 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
+import json
 import pathlib
 import pathlib
 from abc import abstractmethod
 from abc import abstractmethod
 from typing import Any, Dict, Generic, Iterable, List, Optional, TypeVar, Union
 from typing import Any, Dict, Generic, Iterable, List, Optional, TypeVar, Union
 
 
+from ..exceptions import FileCannotBeRead
+from ._decoder import _Decoder
+
 ModelType = TypeVar("ModelType")
 ModelType = TypeVar("ModelType")
 Entity = TypeVar("Entity")
 Entity = TypeVar("Entity")
 
 
@@ -122,3 +126,27 @@ class _AbstractRepository(Generic[ModelType, Entity]):
             folder_path (Union[str, pathlib.Path]): The folder path to export the entity to.
             folder_path (Union[str, pathlib.Path]): The folder path to export the entity to.
         """
         """
         raise NotImplementedError
         raise NotImplementedError
+
+    def _import(self, entity_file_path: pathlib.Path) -> Entity:
+        """
+        Import an entity from an exported file.
+
+        Parameters:
+            folder_path (Union[str, pathlib.Path]): The folder path to export the entity to.
+
+        Returns:
+            The imported entity.
+        """
+        if not entity_file_path.is_file():
+            raise FileNotFoundError
+
+        try:
+            with entity_file_path.open("r", encoding="UTF-8") as f:
+                file_content = f.read()
+        except Exception:
+            raise FileCannotBeRead(str(entity_file_path)) from None
+
+        if isinstance(file_content, str):
+            file_content = json.loads(file_content, cls=_Decoder)
+        model = self.model_type.from_dict(file_content)  # type: ignore[attr-defined]
+        return self.converter._model_to_entity(model)  # type: ignore[attr-defined]

+ 15 - 0
taipy/core/_version/_version_manager.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
+import pathlib
 import uuid
 import uuid
 from typing import List, Optional, Union
 from typing import List, Optional, Union
 
 
@@ -230,3 +231,17 @@ class _VersionManager(_Manager[_Version]):
     @classmethod
     @classmethod
     def _delete_entities_of_multiple_types(cls, _entity_ids):
     def _delete_entities_of_multiple_types(cls, _entity_ids):
         raise NotImplementedError
         raise NotImplementedError
+
+    @classmethod
+    def _import(cls, entity_file: pathlib.Path, version: str, **kwargs) -> _Version:
+        imported_version = cls._repository._import(entity_file)
+
+        comparator_result = Config._comparator._find_conflict_config(  # type: ignore[attr-defined]
+            imported_version.config,
+            Config._applied_config,  # type: ignore[attr-defined]
+            imported_version.id,
+        )
+        if comparator_result.get(_ComparatorResult.CONFLICTED_SECTION_KEY):
+            raise ConflictedConfigurationError()
+
+        return imported_version

+ 3 - 2
taipy/core/_version/_version_manager_factory.py

@@ -9,6 +9,8 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
+from typing import Type
+
 from .._manager._manager_factory import _ManagerFactory
 from .._manager._manager_factory import _ManagerFactory
 from ..common import _utils
 from ..common import _utils
 from ._version_fs_repository import _VersionFSRepository
 from ._version_fs_repository import _VersionFSRepository
@@ -17,11 +19,10 @@ from ._version_sql_repository import _VersionSQLRepository
 
 
 
 
 class _VersionManagerFactory(_ManagerFactory):
 class _VersionManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _VersionFSRepository, "sql": _VersionSQLRepository}
     __REPOSITORY_MAP = {"default": _VersionFSRepository, "sql": _VersionSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> _VersionManager:  # type: ignore
+    def _build_manager(cls) -> Type[_VersionManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             version_manager = _utils._load_fct(
             version_manager = _utils._load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + "._version._version_manager", "_VersionManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + "._version._version_manager", "_VersionManager"

+ 1 - 2
taipy/core/cycle/_cycle_manager_factory.py

@@ -19,11 +19,10 @@ from ._cycle_sql_repository import _CycleSQLRepository
 
 
 
 
 class _CycleManagerFactory(_ManagerFactory):
 class _CycleManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _CycleFSRepository, "sql": _CycleSQLRepository}
     __REPOSITORY_MAP = {"default": _CycleFSRepository, "sql": _CycleSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_CycleManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_CycleManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             cycle_manager = _load_fct(
             cycle_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".cycle._cycle_manager", "_CycleManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".cycle._cycle_manager", "_CycleManager"

+ 20 - 2
taipy/core/data/_data_manager.py

@@ -181,10 +181,28 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         else:
         else:
             folder = folder_path
             folder = folder_path
 
 
-        data_export_dir = folder / Config.core.storage_folder
+        data_export_dir = folder / Config.core.storage_folder / os.path.dirname(data_node.path)
         if not data_export_dir.exists():
         if not data_export_dir.exists():
             data_export_dir.mkdir(parents=True)
             data_export_dir.mkdir(parents=True)
 
 
         data_export_path = data_export_dir / os.path.basename(data_node.path)
         data_export_path = data_export_dir / os.path.basename(data_node.path)
         if os.path.exists(data_node.path):
         if os.path.exists(data_node.path):
-            shutil.copy(data_node.path, data_export_path)
+            shutil.copy2(data_node.path, data_export_path)
+
+    @classmethod
+    def _import(cls, entity_file: pathlib.Path, version: str, **kwargs) -> DataNode:
+        imported_data_node = cls._repository._import(entity_file)
+        imported_data_node._version = version
+        cls._set(imported_data_node)
+
+        if not (isinstance(imported_data_node, _FileDataNodeMixin) and isinstance(imported_data_node, DataNode)):
+            return imported_data_node
+
+        data_folder: pathlib.Path = pathlib.Path(str(kwargs.get("data_folder")))
+        if not data_folder.exists():
+            return imported_data_node
+
+        if (data_folder / imported_data_node.path).exists():
+            shutil.copy2(data_folder / imported_data_node.path, imported_data_node.path)
+
+        return imported_data_node

+ 1 - 2
taipy/core/data/_data_manager_factory.py

@@ -19,11 +19,10 @@ from ._data_sql_repository import _DataSQLRepository
 
 
 
 
 class _DataManagerFactory(_ManagerFactory):
 class _DataManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _DataFSRepository, "sql": _DataSQLRepository}
     __REPOSITORY_MAP = {"default": _DataFSRepository, "sql": _DataSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_DataManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_DataManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             data_manager = _load_fct(
             data_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".data._data_manager", "_DataManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".data._data_manager", "_DataManager"

+ 4 - 4
taipy/core/data/_filter.py

@@ -135,7 +135,7 @@ class _FilterDataNode:
         elif join_operator == JoinOperator.OR:
         elif join_operator == JoinOperator.OR:
             how = "outer"
             how = "outer"
         else:
         else:
-            return NotImplementedError
+            raise NotImplementedError
 
 
         filtered_df_data = [
         filtered_df_data = [
             _FilterDataNode.__filter_dataframe_per_key_value(df_data, key, value, operator)
             _FilterDataNode.__filter_dataframe_per_key_value(df_data, key, value, operator)
@@ -177,7 +177,7 @@ class _FilterDataNode:
         elif join_operator == JoinOperator.OR:
         elif join_operator == JoinOperator.OR:
             join_conditions = reduce(or_, conditions)
             join_conditions = reduce(or_, conditions)
         else:
         else:
-            return NotImplementedError
+            raise NotImplementedError
 
 
         return data[join_conditions]
         return data[join_conditions]
 
 
@@ -199,7 +199,7 @@ class _FilterDataNode:
         if operator == Operator.GREATER_OR_EQUAL:
         if operator == Operator.GREATER_OR_EQUAL:
             return array_data[:, key] >= value
             return array_data[:, key] >= value
 
 
-        return NotImplementedError
+        raise NotImplementedError
 
 
     @staticmethod
     @staticmethod
     def __filter_list(list_data: List, operators: Union[List, Tuple], join_operator=JoinOperator.AND):
     def __filter_list(list_data: List, operators: Union[List, Tuple], join_operator=JoinOperator.AND):
@@ -218,7 +218,7 @@ class _FilterDataNode:
                 return list({frozenset(item.items()) for item in merged_list})
                 return list({frozenset(item.items()) for item in merged_list})
             return list(set(merged_list))
             return list(set(merged_list))
         else:
         else:
-            return NotImplementedError
+            raise NotImplementedError
 
 
     @staticmethod
     @staticmethod
     def __filter_list_per_key_value(list_data: List, key: str, value, operator: Operator):
     def __filter_list_per_key_value(list_data: List, key: str, value, operator: Operator):

+ 15 - 14
taipy/core/data/data_node.py

@@ -540,20 +540,21 @@ class DataNode(_Entity, _Labeled):
             or the selected data is invalid.<br/>
             or the selected data is invalid.<br/>
             True otherwise.
             True otherwise.
         """
         """
-
-        from ..scenario.scenario import Scenario
-        from ..taipy import get_parents
-
-        parent_scenarios: Set[Scenario] = get_parents(self)["scenario"]  # type: ignore
-        for parent_scenario in parent_scenarios:
-            for ancestor_node in nx.ancestors(parent_scenario._build_dag(), self):
-                if (
-                    isinstance(ancestor_node, DataNode)
-                    and ancestor_node.last_edit_date
-                    and ancestor_node.last_edit_date > self.last_edit_date
-                ):
-                    return False
-        return self.is_valid
+        if self.is_valid:
+            from ..scenario.scenario import Scenario
+            from ..taipy import get_parents
+
+            parent_scenarios: Set[Scenario] = get_parents(self)["scenario"]  # type: ignore
+            for parent_scenario in parent_scenarios:
+                for ancestor_node in nx.ancestors(parent_scenario._build_dag(), self):
+                    if (
+                        isinstance(ancestor_node, DataNode)
+                        and ancestor_node.last_edit_date
+                        and ancestor_node.last_edit_date > self.last_edit_date
+                    ):
+                        return False
+            return True
+        return False
 
 
     @staticmethod
     @staticmethod
     def _class_map():
     def _class_map():

+ 35 - 8
taipy/core/exceptions/exceptions.py

@@ -261,7 +261,7 @@ class NonExistingScenarioConfig(Exception):
         self.message = f"Scenario config: {scenario_config_id} does not exist."
         self.message = f"Scenario config: {scenario_config_id} does not exist."
 
 
 
 
-class InvalidSscenario(Exception):
+class InvalidScenario(Exception):
     """Raised if a Scenario is not a Directed Acyclic Graph."""
     """Raised if a Scenario is not a Directed Acyclic Graph."""
 
 
     def __init__(self, scenario_id: str):
     def __init__(self, scenario_id: str):
@@ -339,10 +339,6 @@ class ModeNotAvailable(Exception):
     """Raised if the mode in JobConfig is not supported."""
     """Raised if the mode in JobConfig is not supported."""
 
 
 
 
-class InvalidExportPath(Exception):
-    """Raised if the export path is not valid."""
-
-
 class NonExistingVersion(Exception):
 class NonExistingVersion(Exception):
     """Raised if request a Version that is not known by the Version Manager."""
     """Raised if request a Version that is not known by the Version Manager."""
 
 
@@ -373,16 +369,47 @@ class FileCannotBeRead(Exception):
     """Raised when a file cannot be read."""
     """Raised when a file cannot be read."""
 
 
 
 
-class ExportFolderAlreadyExists(Exception):
+class ExportPathAlreadyExists(Exception):
     """Raised when the export folder already exists."""
     """Raised when the export folder already exists."""
 
 
-    def __init__(self, folder_path: str, scenario_id: str):
+    def __init__(self, export_path: str, scenario_id: str):
         self.message = (
         self.message = (
-            f"Folder '{folder_path}' already exists and can not be used to export scenario '{scenario_id}'."
+            f"The path '{export_path}' already exists and can not be used to export scenario '{scenario_id}'."
             " Please use the 'override' parameter to override it."
             " Please use the 'override' parameter to override it."
         )
         )
 
 
 
 
+class EntitiesToBeImportAlredyExist(Exception):
+    """Raised when entities in the scenario to be imported have already exists"""
+
+    def __init__(self, import_path):
+        self.message = f"The import archive file {import_path} contains entities that have already existed."
+
+
+class DataToBeImportAlredyExist(Exception):
+    """Raised when data files in the scenario to be imported have already exists"""
+
+    def __init__(self, import_path):
+        self.message = (
+            f"The import archive file {import_path} contains data files that have already existed."
+            " Please use the 'override' parameter to override those."
+        )
+
+
+class ImportArchiveDoesntContainAnyScenario(Exception):
+    """Raised when the import archive file doesn't contain any scenario"""
+
+    def __init__(self, import_path):
+        self.message = f"The import archive file {import_path} doesn't contain any scenario."
+
+
+class ImportScenarioDoesntHaveAVersion(Exception):
+    """Raised when the import scenario doesn't have a version"""
+
+    def __init__(self, import_path):
+        self.message = f"The import scenario in the import archive file {import_path} doesn't have a version."
+
+
 class SQLQueryCannotBeExecuted(Exception):
 class SQLQueryCannotBeExecuted(Exception):
     """Raised when an SQL Query cannot be executed."""
     """Raised when an SQL Query cannot be executed."""
 
 

+ 3 - 1
taipy/core/job/_job_manager.py

@@ -58,7 +58,9 @@ class _JobManager(_Manager[Job], _VersionMixin):
         return job
         return job
 
 
     @classmethod
     @classmethod
-    def _delete(cls, job: Job, force=False):
+    def _delete(cls, job: Union[Job, JobId], force=False):
+        if isinstance(job, str):
+            job = cls._get(job)
         if cls._is_deletable(job) or force:
         if cls._is_deletable(job) or force:
             super()._delete(job.id)
             super()._delete(job.id)
         else:
         else:

+ 1 - 2
taipy/core/job/_job_manager_factory.py

@@ -19,11 +19,10 @@ from ._job_sql_repository import _JobSQLRepository
 
 
 
 
 class _JobManagerFactory(_ManagerFactory):
 class _JobManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _JobFSRepository, "sql": _JobSQLRepository}
     __REPOSITORY_MAP = {"default": _JobFSRepository, "sql": _JobSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_JobManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_JobManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             job_manager = _load_fct(
             job_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".job._job_manager", "_JobManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".job._job_manager", "_JobManager"

+ 92 - 3
taipy/core/scenario/_scenario_manager.py

@@ -10,14 +10,18 @@
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
 import datetime
 import datetime
+import pathlib
+import tempfile
+import zipfile
 from functools import partial
 from functools import partial
-from typing import Any, Callable, List, Literal, Optional, Union
+from typing import Any, Callable, Dict, List, Literal, Optional, Type, Union
 
 
 from taipy.config import Config
 from taipy.config import Config
 
 
 from .._entity._entity_ids import _EntityIds
 from .._entity._entity_ids import _EntityIds
 from .._manager._manager import _Manager
 from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._repository._abstract_repository import _AbstractRepository
+from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.scenario_config import ScenarioConfig
 from ..config.scenario_config import ScenarioConfig
@@ -28,9 +32,12 @@ from ..exceptions.exceptions import (
     DeletingPrimaryScenario,
     DeletingPrimaryScenario,
     DifferentScenarioConfigs,
     DifferentScenarioConfigs,
     DoesNotBelongToACycle,
     DoesNotBelongToACycle,
+    EntitiesToBeImportAlredyExist,
+    ImportArchiveDoesntContainAnyScenario,
+    ImportScenarioDoesntHaveAVersion,
     InsufficientScenarioToCompare,
     InsufficientScenarioToCompare,
+    InvalidScenario,
     InvalidSequence,
     InvalidSequence,
-    InvalidSscenario,
     NonExistingComparator,
     NonExistingComparator,
     NonExistingScenario,
     NonExistingScenario,
     NonExistingScenarioConfig,
     NonExistingScenarioConfig,
@@ -180,7 +187,7 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         cls._set(scenario)
         cls._set(scenario)
 
 
         if not scenario._is_consistent():
         if not scenario._is_consistent():
-            raise InvalidSscenario(scenario.id)
+            raise InvalidScenario(scenario.id)
 
 
         actual_sequences = scenario._get_sequences()
         actual_sequences = scenario._get_sequences()
         for sequence_name in sequences.keys():
         for sequence_name in sequences.keys():
@@ -451,3 +458,85 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         for fil in filters:
         for fil in filters:
             fil.update({"config_id": config_id})
             fil.update({"config_id": config_id})
         return cls._repository._load_all(filters)
         return cls._repository._load_all(filters)
+
+    @classmethod
+    def _import_scenario_and_children_entities(
+        cls,
+        zip_file_path: pathlib.Path,
+        override: bool,
+        entity_managers: Dict[str, Type[_Manager]],
+    ) -> Optional[Scenario]:
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            with zipfile.ZipFile(zip_file_path) as zip_file:
+                zip_file.extractall(tmp_dir)
+
+            tmp_dir_path = pathlib.Path(tmp_dir)
+
+            if not ((tmp_dir_path / "scenarios").exists() or (tmp_dir_path / "scenario").exists()):
+                raise ImportArchiveDoesntContainAnyScenario(zip_file_path)
+
+            if not (tmp_dir_path / "version").exists():
+                raise ImportScenarioDoesntHaveAVersion(zip_file_path)
+
+            # Import the version to check for compatibility
+            entity_managers["version"]._import(next((tmp_dir_path / "version").iterdir()), "")
+
+            valid_entity_folders = list(entity_managers.keys())
+            valid_data_folder = Config.core.storage_folder
+
+            imported_scenario = None
+            imported_entities: Dict[str, List] = {}
+
+            for entity_folder in tmp_dir_path.iterdir():
+                if not entity_folder.is_dir() or entity_folder.name not in valid_entity_folders + [valid_data_folder]:
+                    cls._logger.warning(f"{entity_folder} is not a valid Taipy folder and will not be imported.")
+                    continue
+
+            try:
+                for entity_type in valid_entity_folders:
+                    # Skip the version folder as it is already handled
+                    if entity_type == "version":
+                        continue
+
+                    entity_folder = tmp_dir_path / entity_type
+                    if not entity_folder.exists():
+                        continue
+
+                    manager = entity_managers[entity_type]
+                    imported_entities[entity_type] = []
+
+                    for entity_file in entity_folder.iterdir():
+                        # Check if the to-be-imported entity already exists
+                        entity_id = entity_file.stem
+                        if manager._exists(entity_id):
+                            if override:
+                                cls._logger.warning(f"{entity_id} already exists and will be overridden.")
+                            else:
+                                cls._logger.error(
+                                    f"{entity_id} already exists. Please use the 'override' parameter to override it."
+                                )
+                                raise EntitiesToBeImportAlredyExist(zip_file_path)
+
+                        # Import the entity
+                        imported_entity = manager._import(
+                            entity_file,
+                            version=_VersionManagerFactory._build_manager()._get_latest_version(),
+                            data_folder=tmp_dir_path / valid_data_folder,
+                        )
+
+                        imported_entities[entity_type].append(imported_entity.id)
+                        if entity_type in ["scenario", "scenarios"]:
+                            imported_scenario = imported_entity
+            except Exception as err:
+                cls._logger.error(f"An error occurred during the import: {err}. Rollback the import.")
+
+                # Rollback the import
+                for entity_type, entity_ids in list(imported_entities.items())[::-1]:
+                    manager = entity_managers[entity_type]
+                    for entity_id in entity_ids:
+                        if manager._exists(entity_id):
+                            manager._delete(entity_id)
+                raise err
+
+        cls._logger.info(f"Scenario {imported_scenario.id} has been successfully imported.")  # type: ignore[union-attr]
+        return imported_scenario

+ 1 - 2
taipy/core/scenario/_scenario_manager_factory.py

@@ -19,11 +19,10 @@ from ._scenario_sql_repository import _ScenarioSQLRepository
 
 
 
 
 class _ScenarioManagerFactory(_ManagerFactory):
 class _ScenarioManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _ScenarioFSRepository, "sql": _ScenarioSQLRepository}
     __REPOSITORY_MAP = {"default": _ScenarioFSRepository, "sql": _ScenarioSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_ScenarioManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_ScenarioManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             scenario_manager = _load_fct(
             scenario_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".scenario._scenario_manager", "_ScenarioManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".scenario._scenario_manager", "_ScenarioManager"

+ 1 - 2
taipy/core/submission/_submission_manager_factory.py

@@ -19,11 +19,10 @@ from ._submission_sql_repository import _SubmissionSQLRepository
 
 
 
 
 class _SubmissionManagerFactory(_ManagerFactory):
 class _SubmissionManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _SubmissionFSRepository, "sql": _SubmissionSQLRepository}
     __REPOSITORY_MAP = {"default": _SubmissionFSRepository, "sql": _SubmissionSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_SubmissionManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_SubmissionManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             submission_manager = _load_fct(
             submission_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".submission._submission_manager", "_SubmissionManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".submission._submission_manager", "_SubmissionManager"

+ 111 - 57
taipy/core/taipy.py

@@ -12,14 +12,16 @@
 import os
 import os
 import pathlib
 import pathlib
 import shutil
 import shutil
+import tempfile
 from datetime import datetime
 from datetime import datetime
-from typing import Any, Callable, Dict, List, Literal, Optional, Set, Union, overload
+from typing import Any, Callable, Dict, List, Literal, Optional, Set, Type, Union, overload
 
 
-from taipy.config import Config, Scope
+from taipy.config import Scope
 from taipy.logger._taipy_logger import _TaipyLogger
 from taipy.logger._taipy_logger import _TaipyLogger
 
 
 from ._core import Core
 from ._core import Core
 from ._entity._entity import _Entity
 from ._entity._entity import _Entity
+from ._manager._manager import _Manager
 from ._version._version_manager_factory import _VersionManagerFactory
 from ._version._version_manager_factory import _VersionManagerFactory
 from .common._check_instance import (
 from .common._check_instance import (
     _is_cycle,
     _is_cycle,
@@ -41,8 +43,7 @@ from .data.data_node import DataNode
 from .data.data_node_id import DataNodeId
 from .data.data_node_id import DataNodeId
 from .exceptions.exceptions import (
 from .exceptions.exceptions import (
     DataNodeConfigIsNotGlobal,
     DataNodeConfigIsNotGlobal,
-    ExportFolderAlreadyExists,
-    InvalidExportPath,
+    ExportPathAlreadyExists,
     ModelNotFound,
     ModelNotFound,
     NonExistingVersion,
     NonExistingVersion,
     VersionIsNotProductionVersion,
     VersionIsNotProductionVersion,
@@ -65,7 +66,7 @@ from .task.task_id import TaskId
 __logger = _TaipyLogger._get_logger()
 __logger = _TaipyLogger._get_logger()
 
 
 
 
-def set(entity: Union[DataNode, Task, Sequence, Scenario, Cycle]):
+def set(entity: Union[DataNode, Task, Sequence, Scenario, Cycle, Submission]):
     """Save or update an entity.
     """Save or update an entity.
 
 
     This function allows you to save or update an entity in Taipy.
     This function allows you to save or update an entity in Taipy.
@@ -518,24 +519,25 @@ def get_scenarios(
     """Retrieve a list of existing scenarios filtered by cycle or tag.
     """Retrieve a list of existing scenarios filtered by cycle or tag.
 
 
     This function allows you to retrieve a list of scenarios based on optional
     This function allows you to retrieve a list of scenarios based on optional
-    filtering criteria. If both a _cycle_ and a _tag_ are provided, the returned
-    list contains scenarios that belong to the specified _cycle_ **and** also
-    have the specified _tag_.
+    filtering criteria. If both a *cycle* and a *tag* are provided, the returned
+    list contains scenarios that belong to the specified *cycle* and also
+    have the specified *tag*.
 
 
     Parameters:
     Parameters:
-         cycle (Optional[Cycle^]): The optional `Cycle^` to filter scenarios by.
-         tag (Optional[str]): The optional tag to filter scenarios by.
-         is_sorted (bool): The option to sort scenarios. The default sorting key is name.
-         descending (bool): The option to sort scenarios on the sorting key in descending order.
-         sort_key (Literal["name", "id", "creation_date", "tags"]): The optiononal sort_key to
-             decide upon what key scenarios are sorted. The sorting is in increasing order for
-             dates, in alphabetical order for name and id, in lexographical order for tags.
+        cycle (Optional[Cycle^]): The optional `Cycle^` to filter scenarios by.
+        tag (Optional[str]): The optional tag to filter scenarios by.
+        is_sorted (bool): If True, sort the output list of scenarios using the sorting key.
+            The default value is False.
+        descending (bool): If True, sort the output list of scenarios in descending order.
+            The default value is False.
+        sort_key (Literal["name", "id", "creation_date", "tags"]): The optiononal sort_key to
+            decide upon what key scenarios are sorted. The sorting is in increasing order for
+            dates, in alphabetical order for name and id, and in lexicographical order for tags.
+            The default value is "name".<br/>
+            If an incorrect sorting key is provided, the scenarios are sorted by name.
 
 
     Returns:
     Returns:
-        The list of scenarios filtered by cycle or tag and optionally sorted by name, id, creation_date or tags.
-            If no filtering criterion is provided, this method returns all existing scenarios.
-            If is_sorted is set to true, the scenarios are sorted by sort_key. The scenarios
-            are sorted by name if an incorrect or no sort_key is provided.
+        The list of scenarios filtered by cycle or tag.
     """
     """
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenario_manager = _ScenarioManagerFactory._build_manager()
     if not cycle and not tag:
     if not cycle and not tag:
@@ -576,17 +578,18 @@ def get_primary_scenarios(
     """Retrieve a list of all primary scenarios.
     """Retrieve a list of all primary scenarios.
 
 
     Parameters:
     Parameters:
-         is_sorted (bool): The option to sort scenarios. The default sorting key is name.
-         descending (bool): The option to sort scenarios on the sorting key in descending order.
-         sort_key (Literal["name", "id", "creation_date", "tags"]): The optiononal sort_key to
-             decide upon what key scenarios are sorted. The sorting is in increasing order for
-             dates, in alphabetical order for name and id, in lexographical order for tags.
+        is_sorted (bool): If True, sort the output list of scenarios using the sorting key.
+            The default value is False.
+        descending (bool): If True, sort the output list of scenarios in descending order.
+            The default value is False.
+        sort_key (Literal["name", "id", "creation_date", "tags"]): The optiononal sort_key to
+            decide upon what key scenarios are sorted. The sorting is in increasing order for
+            dates, in alphabetical order for name and id, and in lexicographical order for tags.
+            The default value is "name".<br/>
+            If an incorrect sorting key is provided, the scenarios are sorted by name.
 
 
     Returns:
     Returns:
-        The list containing all primary scenarios, optionally sorted by name, id, creation_date or tags.
-            The sorting is in increasing order for dates, in alphabetical order for name and
-            id, and in lexicographical order for tags. If sorted is set to true, but if an
-            incorrect or no sort_key is provided, the scenarios are sorted by name.
+        A list contains all primary scenarios.
     """
     """
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenarios = scenario_manager._get_primary_scenarios()
     scenarios = scenario_manager._get_primary_scenarios()
@@ -595,7 +598,6 @@ def get_primary_scenarios(
     return scenarios
     return scenarios
 
 
 
 
-
 def is_promotable(scenario: Union[Scenario, ScenarioId]) -> bool:
 def is_promotable(scenario: Union[Scenario, ScenarioId]) -> bool:
     """Determine if a scenario can be promoted to become a primary scenario.
     """Determine if a scenario can be promoted to become a primary scenario.
 
 
@@ -981,27 +983,28 @@ def clean_all_entities(version_number: str) -> bool:
 
 
 def export_scenario(
 def export_scenario(
     scenario_id: ScenarioId,
     scenario_id: ScenarioId,
-    folder_path: Union[str, pathlib.Path],
+    output_path: Union[str, pathlib.Path],
     override: bool = False,
     override: bool = False,
     include_data: bool = False,
     include_data: bool = False,
 ):
 ):
-    """Export all related entities of a scenario to a folder.
+    """Export all related entities of a scenario to a archive zip file.
 
 
     This function exports all related entities of the specified scenario to the
     This function exports all related entities of the specified scenario to the
-    specified folder.
+    specified archive zip file.
 
 
     Parameters:
     Parameters:
         scenario_id (ScenarioId): The ID of the scenario to export.
         scenario_id (ScenarioId): The ID of the scenario to export.
-        folder_path (Union[str, pathlib.Path]): The folder path to export the scenario to.
+        output_path (Union[str, pathlib.Path]): The path to export the scenario to.
+            The path should include the file name without the extension or with the `.zip` extension.
             If the path exists and the override parameter is False, an exception is raised.
             If the path exists and the override parameter is False, an exception is raised.
-        override (bool): If True, the existing folder will be overridden. Default is False.
+        override (bool): If True, the existing folder will be overridden. The default value is False.
         include_data (bool): If True, the file-based data nodes are exported as well.
         include_data (bool): If True, the file-based data nodes are exported as well.
             This includes Pickle, CSV, Excel, Parquet, and JSON data nodes.
             This includes Pickle, CSV, Excel, Parquet, and JSON data nodes.
             If the scenario has a data node that is not file-based, a warning will be logged, and the data node
             If the scenario has a data node that is not file-based, a warning will be logged, and the data node
             will not be exported. The default value is False.
             will not be exported. The default value is False.
 
 
     Raises:
     Raises:
-        ExportFolderAlreadyExist^: If the `folder_path` already exists and the override parameter is False.
+        ExportPathAlreadyExists^: If the `output_path` already exists and the override parameter is False.
     """
     """
     manager = _ScenarioManagerFactory._build_manager()
     manager = _ScenarioManagerFactory._build_manager()
     scenario = manager._get(scenario_id)
     scenario = manager._get(scenario_id)
@@ -1010,31 +1013,82 @@ def export_scenario(
     if scenario.cycle:
     if scenario.cycle:
         entity_ids.cycle_ids = {scenario.cycle.id}
         entity_ids.cycle_ids = {scenario.cycle.id}
 
 
-    if folder_path == Config.core.taipy_storage_folder:
-        raise InvalidExportPath("The export folder must not be the storage folder.")
+    output_filename = os.path.splitext(output_path)[0] if str(output_path).endswith(".zip") else str(output_path)
+    output_zip_path = pathlib.Path(output_filename + ".zip")
 
 
-    if os.path.exists(folder_path):
+    if output_zip_path.exists():
         if override:
         if override:
-            __logger.warning(f"Override the existing folder '{folder_path}'")
-            shutil.rmtree(folder_path, ignore_errors=True)
+            __logger.warning(f"Override the existing path '{output_zip_path}' to export scenario {scenario_id}.")
+            output_zip_path.unlink()
         else:
         else:
-            raise ExportFolderAlreadyExists(str(folder_path), scenario_id)
-
-    for data_node_id in entity_ids.data_node_ids:
-        _DataManagerFactory._build_manager()._export(data_node_id, folder_path, include_data=include_data)
-    for task_id in entity_ids.task_ids:
-        _TaskManagerFactory._build_manager()._export(task_id, folder_path)
-    for sequence_id in entity_ids.sequence_ids:
-        _SequenceManagerFactory._build_manager()._export(sequence_id, folder_path)
-    for cycle_id in entity_ids.cycle_ids:
-        _CycleManagerFactory._build_manager()._export(cycle_id, folder_path)
-    for scenario_id in entity_ids.scenario_ids:
-        _ScenarioManagerFactory._build_manager()._export(scenario_id, folder_path)
-    for job_id in entity_ids.job_ids:
-        _JobManagerFactory._build_manager()._export(job_id, folder_path)
-    for submission_id in entity_ids.submission_ids:
-        _SubmissionManagerFactory._build_manager()._export(submission_id, folder_path)
-    _VersionManagerFactory._build_manager()._export(scenario.version, folder_path)
+            raise ExportPathAlreadyExists(str(output_zip_path), scenario_id)
+
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        for data_node_id in entity_ids.data_node_ids:
+            _DataManagerFactory._build_manager()._export(data_node_id, tmp_dir, include_data=include_data)
+        for task_id in entity_ids.task_ids:
+            _TaskManagerFactory._build_manager()._export(task_id, tmp_dir)
+        for sequence_id in entity_ids.sequence_ids:
+            _SequenceManagerFactory._build_manager()._export(sequence_id, tmp_dir)
+        for cycle_id in entity_ids.cycle_ids:
+            _CycleManagerFactory._build_manager()._export(cycle_id, tmp_dir)
+        for scenario_id in entity_ids.scenario_ids:
+            _ScenarioManagerFactory._build_manager()._export(scenario_id, tmp_dir)
+        for job_id in entity_ids.job_ids:
+            _JobManagerFactory._build_manager()._export(job_id, tmp_dir)
+        for submission_id in entity_ids.submission_ids:
+            _SubmissionManagerFactory._build_manager()._export(submission_id, tmp_dir)
+        _VersionManagerFactory._build_manager()._export(scenario.version, tmp_dir)
+
+        shutil.make_archive(output_filename, "zip", tmp_dir)
+
+
+def import_scenario(input_path: Union[str, pathlib.Path], override: bool = False) -> Optional[Scenario]:
+    """Import from an archive zip file containing an exported scenario into the current Taipy application.
+
+    The zip file should be created by the `taipy.import()^` method, which contains all related entities
+    of the scenario.
+    All entities should belong to the same version that is compatible with the current Taipy application version.
+
+    Parameters:
+        input_path (Union[str, pathlib.Path]): The path to the archive scenario to import.
+            If the path doesn't exist, an exception is raised.
+        override (bool): If True, override the entities if existed. The default value is False.
+
+    Return:
+        The imported scenario.
+
+    Raises:
+        FileNotFoundError: If the import path does not exist.
+        ImportArchiveDoesntContainAnyScenario: If the unzip folder doesn't contain any scenario.
+        ConflictedConfigurationError: If the configuration of the imported scenario is conflicted with the current one.
+    """
+    if isinstance(input_path, str):
+        zip_file_path: pathlib.Path = pathlib.Path(input_path)
+    else:
+        zip_file_path = input_path
+
+    if not zip_file_path.exists():
+        raise FileNotFoundError(f"The import archive path '{zip_file_path}' does not exist.")
+
+    entity_managers: Dict[str, Type[_Manager]] = {
+        "cycles": _CycleManagerFactory._build_manager(),
+        "cycle": _CycleManagerFactory._build_manager(),
+        "data_nodes": _DataManagerFactory._build_manager(),
+        "data_node": _DataManagerFactory._build_manager(),
+        "tasks": _TaskManagerFactory._build_manager(),
+        "task": _TaskManagerFactory._build_manager(),
+        "scenarios": _ScenarioManagerFactory._build_manager(),
+        "scenario": _ScenarioManagerFactory._build_manager(),
+        "jobs": _JobManagerFactory._build_manager(),
+        "job": _JobManagerFactory._build_manager(),
+        "submission": _SubmissionManagerFactory._build_manager(),
+        "version": _VersionManagerFactory._build_manager(),
+    }
+
+    return _ScenarioManagerFactory._build_manager()._import_scenario_and_children_entities(
+        zip_file_path, override, entity_managers
+    )
 
 
 
 
 def get_parents(
 def get_parents(

+ 1 - 2
taipy/core/task/_task_manager_factory.py

@@ -19,11 +19,10 @@ from ._task_sql_repository import _TaskSQLRepository
 
 
 
 
 class _TaskManagerFactory(_ManagerFactory):
 class _TaskManagerFactory(_ManagerFactory):
-
     __REPOSITORY_MAP = {"default": _TaskFSRepository, "sql": _TaskSQLRepository}
     __REPOSITORY_MAP = {"default": _TaskFSRepository, "sql": _TaskSQLRepository}
 
 
     @classmethod
     @classmethod
-    def _build_manager(cls) -> Type[_TaskManager]:  # type: ignore
+    def _build_manager(cls) -> Type[_TaskManager]:
         if cls._using_enterprise():
         if cls._using_enterprise():
             task_manager = _load_fct(
             task_manager = _load_fct(
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".task._task_manager", "_TaskManager"
                 cls._TAIPY_ENTERPRISE_CORE_MODULE + ".task._task_manager", "_TaskManager"

+ 5 - 2
taipy/gui/_renderers/builder.py

@@ -1005,12 +1005,14 @@ class _Builder:
             elif var_type == PropertyType.toHtmlContent:
             elif var_type == PropertyType.toHtmlContent:
                 self.__set_html_content(attr[0], "page", var_type)
                 self.__set_html_content(attr[0], "page", var_type)
             elif isclass(var_type) and issubclass(var_type, _TaipyBase):
             elif isclass(var_type) and issubclass(var_type, _TaipyBase):
+                prop_name = _to_camel_case(attr[0])
                 if hash_name := self.__hashes.get(attr[0]):
                 if hash_name := self.__hashes.get(attr[0]):
-                    prop_name = _to_camel_case(attr[0])
                     expr = self.__gui._get_expr_from_hash(hash_name)
                     expr = self.__gui._get_expr_from_hash(hash_name)
                     hash_name = self.__gui._evaluate_bind_holder(var_type, expr)
                     hash_name = self.__gui._evaluate_bind_holder(var_type, expr)
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__set_react_attribute(prop_name, hash_name)
                     self.__set_react_attribute(prop_name, hash_name)
+                else:
+                    self.set_attribute(prop_name, var_type(self.__attributes.get(attr[0]), "").get())
 
 
         self.__set_refresh_on_update()
         self.__set_refresh_on_update()
         return self
         return self
@@ -1025,7 +1027,8 @@ class _Builder:
             name (str): The name of the attribute.
             name (str): The name of the attribute.
             value (Any): The value of the attribute (must be json serializable).
             value (Any): The value of the attribute (must be json serializable).
         """
         """
-        self.el.set(name, value)
+        if value is not None:
+            self.el.set(name, value)
         return self
         return self
 
 
     def get_element(self):
     def get_element(self):

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

@@ -79,7 +79,7 @@ class Decimator(ABC):
                 from *data* should be preserved, or False requires that this
                 from *data* should be preserved, or False requires that this
                 data point be dropped.
                 data point be dropped.
         """
         """
-        return NotImplementedError  # type: ignore
+        raise NotImplementedError
 
 
 
 
 def _df_data_filter(
 def _df_data_filter(

+ 11 - 4
taipy/gui/extension/library.py

@@ -159,12 +159,17 @@ class Element:
         properties: t.Optional[t.Dict[str, t.Any]],
         properties: t.Optional[t.Dict[str, t.Any]],
         lib: "ElementLibrary",
         lib: "ElementLibrary",
         is_html: t.Optional[bool] = False,
         is_html: t.Optional[bool] = False,
-        counter: int = 0
+        counter: int = 0,
     ) -> t.Union[t.Any, t.Tuple[str, str]]:
     ) -> t.Union[t.Any, t.Tuple[str, str]]:
         attributes = properties if isinstance(properties, dict) else {}
         attributes = properties if isinstance(properties, dict) else {}
         if self.inner_properties:
         if self.inner_properties:
             uniques: t.Dict[str, int] = {}
             uniques: t.Dict[str, int] = {}
-            self.attributes.update(self.inner_properties)
+            self.attributes.update(
+                {
+                    prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update)
+                    for prop, attr in self.inner_properties.items()
+                }
+            )
             for prop, attr in self.inner_properties.items():
             for prop, attr in self.inner_properties.items():
                 val = attr.default_value
                 val = attr.default_value
                 if val:
                 if val:
@@ -184,7 +189,9 @@ class Element:
                         if id is None:
                         if id is None:
                             id = len(uniques) + 1
                             id = len(uniques) + 1
                             uniques[m.group(1)] = id
                             uniques[m.group(1)] = id
-                        val = f"{val[: m.start()]}'{counter}.{id}'{val[m.end() :]}"
+                        val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
+                        if gui._is_expression(val):
+                            gui._evaluate_expr(val, True)
 
 
                 attributes[prop] = val
                 attributes[prop] = val
         # this modifies attributes
         # this modifies attributes
@@ -286,7 +293,7 @@ class ElementLibrary(ABC):
             because each JavaScript module will have to have a unique name.
             because each JavaScript module will have to have a unique name.
 
 
         """
         """
-        return NotImplementedError  # type: ignore
+        raise NotImplementedError
 
 
     def get_js_module_name(self) -> str:
     def get_js_module_name(self) -> str:
         """
         """

+ 3 - 3
taipy/gui/gui.py

@@ -1031,7 +1031,7 @@ class Gui:
                     json.dumps(newvalue, cls=_TaipyJsonEncoder)
                     json.dumps(newvalue, cls=_TaipyJsonEncoder)
                     if len(warns):
                     if len(warns):
                         keep_value = True
                         keep_value = True
-                        for w in list(warns):
+                        for w in warns:
                             if is_debugging():
                             if is_debugging():
                                 debug_warnings.append(w)
                                 debug_warnings.append(w)
                             if w.category is not DeprecationWarning and w.category is not PendingDeprecationWarning:
                             if w.category is not DeprecationWarning and w.category is not PendingDeprecationWarning:
@@ -1468,8 +1468,8 @@ class Gui:
             return False
             return False
 
 
     # Proxy methods for Evaluator
     # Proxy methods for Evaluator
-    def _evaluate_expr(self, expr: str) -> t.Any:
-        return self.__evaluator.evaluate_expr(self, expr)
+    def _evaluate_expr(self, expr: str, lazy_declare: t.Optional[bool] = False) -> t.Any:
+        return self.__evaluator.evaluate_expr(self, expr, lazy_declare)
 
 
     def _re_evaluate_expr(self, var_name: str) -> t.Set[str]:
     def _re_evaluate_expr(self, var_name: str) -> t.Set[str]:
         return self.__evaluator.re_evaluate_expr(self, var_name)
         return self.__evaluator.re_evaluate_expr(self, var_name)

+ 3 - 2
taipy/gui/utils/_adapter.py

@@ -12,6 +12,7 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import typing as t
 import typing as t
+from operator import add
 
 
 from .._warnings import _warn
 from .._warnings import _warn
 from ..icon import Icon
 from ..icon import Icon
@@ -129,8 +130,8 @@ class _Adapter:
                 if not id_only and len(tpl_res) > 2 and isinstance(tpl_res[2], list) and len(tpl_res[2]) > 0:
                 if not id_only and len(tpl_res) > 2 and isinstance(tpl_res[2], list) and len(tpl_res[2]) > 0:
                     tpl_res = (tpl_res[0], tpl_res[1], self.__on_tree(adapter, tpl_res[2]))
                     tpl_res = (tpl_res[0], tpl_res[1], self.__on_tree(adapter, tpl_res[2]))
                 return (
                 return (
-                    (tpl_res + result[len(tpl_res) :])
-                    if isinstance(result, tuple) and isinstance(tpl_res, tuple)
+                    add(type(result)(tpl_res), result[len(tpl_res) :])
+                    if isinstance(result, (tuple, list)) and isinstance(tpl_res, (tuple, list))
                     else tpl_res
                     else tpl_res
                 )
                 )
         except Exception as e:
         except Exception as e:

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

@@ -15,8 +15,9 @@ import ast
 import builtins
 import builtins
 import re
 import re
 import typing as t
 import typing as t
+import warnings
 
 
-from .._warnings import _warn
+from .._warnings import TaipyGuiWarning, _warn
 
 
 if t.TYPE_CHECKING:
 if t.TYPE_CHECKING:
     from ..gui import Gui
     from ..gui import Gui
@@ -84,7 +85,9 @@ class _Evaluator:
     def _fetch_expression_list(self, expr: str) -> t.List:
     def _fetch_expression_list(self, expr: str) -> t.List:
         return [v[0] for v in _Evaluator.__EXPR_RE.findall(expr)]
         return [v[0] for v in _Evaluator.__EXPR_RE.findall(expr)]
 
 
-    def _analyze_expression(self, gui: Gui, expr: str) -> t.Tuple[t.Dict[str, t.Any], t.Dict[str, str]]:
+    def _analyze_expression(
+        self, gui: Gui, expr: str, lazy_declare: t.Optional[bool] = False
+    ) -> t.Tuple[t.Dict[str, t.Any], t.Dict[str, str]]:
         var_val: t.Dict[str, t.Any] = {}
         var_val: t.Dict[str, t.Any] = {}
         var_map: t.Dict[str, str] = {}
         var_map: t.Dict[str, str] = {}
         non_vars = list(self.__global_ctx.keys())
         non_vars = list(self.__global_ctx.keys())
@@ -105,7 +108,14 @@ class _Evaluator:
                     var_name = node.id.split(sep=".")[0]
                     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 not in args and var_name not in targets and var_name not in non_vars:
                         try:
                         try:
-                            encoded_var_name = gui._bind_var(var_name)
+                            if lazy_declare and var_name.startswith("__"):
+                                with warnings.catch_warnings(record=True) as warns:
+                                    warnings.resetwarnings()
+                                    encoded_var_name = gui._bind_var(var_name)
+                                    if next((w for w in warns if w.category is TaipyGuiWarning), None):
+                                        gui._bind_var_val(var_name, None)
+                            else:
+                                encoded_var_name = gui._bind_var(var_name)
                             var_val[var_name] = _getscopeattr_drill(gui, encoded_var_name)
                             var_val[var_name] = _getscopeattr_drill(gui, encoded_var_name)
                             var_map[var_name] = encoded_var_name
                             var_map[var_name] = encoded_var_name
                         except AttributeError as e:
                         except AttributeError as e:
@@ -200,10 +210,10 @@ class _Evaluator:
             _warn(f"Cannot evaluate expression {holder.__name__}({expr_hash},'{expr_hash}') for {expr}", e)
             _warn(f"Cannot evaluate expression {holder.__name__}({expr_hash},'{expr_hash}') for {expr}", e)
         return None
         return None
 
 
-    def evaluate_expr(self, gui: Gui, expr: str) -> t.Any:
+    def evaluate_expr(self, gui: Gui, expr: str, lazy_declare: t.Optional[bool] = False) -> t.Any:
         if not self._is_expression(expr):
         if not self._is_expression(expr):
             return expr
             return expr
-        var_val, var_map = self._analyze_expression(gui, expr)
+        var_val, var_map = self._analyze_expression(gui, expr, lazy_declare)
         expr_hash = None
         expr_hash = None
         is_edge_case = False
         is_edge_case = False
 
 

+ 3 - 2
taipy/gui/utils/types.py

@@ -12,7 +12,7 @@
 
 
 import json
 import json
 import typing as t
 import typing as t
-from abc import ABC
+from abc import ABC, abstractmethod
 from datetime import datetime
 from datetime import datetime
 from importlib.util import find_spec
 from importlib.util import find_spec
 
 
@@ -55,8 +55,9 @@ class _TaipyBase(ABC):
             return self.__hash_name
             return self.__hash_name
 
 
     @staticmethod
     @staticmethod
+    @abstractmethod
     def get_hash():
     def get_hash():
-        return NotImplementedError
+        raise NotImplementedError
 
 
     @staticmethod
     @staticmethod
     def _get_holder_prefixes() -> t.List[str]:
     def _get_holder_prefixes() -> t.List[str]:

+ 63 - 46
taipy/gui_core/_GuiCoreLib.py

@@ -12,7 +12,7 @@
 import typing as t
 import typing as t
 from datetime import datetime
 from datetime import datetime
 
 
-from taipy.gui import Gui, State
+from taipy.gui import Gui
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 
 
 from ..version import _get_version
 from ..version import _get_version
@@ -20,6 +20,7 @@ from ._adapters import (
     _GuiCoreDatanodeAdapter,
     _GuiCoreDatanodeAdapter,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
     _GuiCoreScenarioDagAdapter,
+    _GuiCoreScenarioProperties,
 )
 )
 from ._context import _GuiCoreContext
 from ._context import _GuiCoreContext
 
 
@@ -30,18 +31,21 @@ class _GuiCore(ElementLibrary):
     __SCENARIO_ADAPTER = "tgc_scenario"
     __SCENARIO_ADAPTER = "tgc_scenario"
     __DATANODE_ADAPTER = "tgc_datanode"
     __DATANODE_ADAPTER = "tgc_datanode"
     __JOB_ADAPTER = "tgc_job"
     __JOB_ADAPTER = "tgc_job"
-    __INNER_VARS = (
-        _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
-        _GuiCoreContext._SCENARIO_SELECTOR_ID_VAR,
-        _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
-        _GuiCoreContext._JOB_SELECTOR_ERROR_VAR,
-        _GuiCoreContext._DATANODE_VIZ_ERROR_VAR,
-        _GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR,
-        _GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR,
-        _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR,
-        _GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR,
-        _GuiCoreContext._DATANODE_VIZ_PROPERTIES_ID_VAR,
-    )
+
+    __SCENARIO_SELECTOR_ERROR_VAR = "__tpgc_sc_error"
+    __SCENARIO_SELECTOR_ID_VAR = "__tpgc_sc_id"
+    __SCENARIO_SELECTOR_FILTER_VAR = "__tpgc_sc_filter"
+    __SCENARIO_VIZ_ERROR_VAR = "__tpgc_sv_error"
+    __JOB_SELECTOR_ERROR_VAR = "__tpgc_js_error"
+    __DATANODE_VIZ_ERROR_VAR = "__tpgc_dv_error"
+    __DATANODE_VIZ_OWNER_ID_VAR = "__tpgc_dv_owner_id"
+    __DATANODE_VIZ_HISTORY_ID_VAR = "__tpgc_dv_history_id"
+    __DATANODE_VIZ_PROPERTIES_ID_VAR = "__tpgc_dv_properties_id"
+    __DATANODE_VIZ_DATA_ID_VAR = "__tpgc_dv_data_id"
+    __DATANODE_VIZ_DATA_CHART_ID_VAR = "__tpgc_dv_data_chart_id"
+    __DATANODE_VIZ_DATA_NODE_PROP = "data_node"
+    __DATANODE_SEL_SCENARIO_PROP = "scenario"
+    __SEL_SCENARIOS_PROP = "scenarios"
 
 
     __elts = {
     __elts = {
         "scenario_selector": Element(
         "scenario_selector": Element(
@@ -58,24 +62,34 @@ class _GuiCore(ElementLibrary):
                 "show_pins": ElementProperty(PropertyType.boolean, False),
                 "show_pins": ElementProperty(PropertyType.boolean, False),
                 "on_creation": ElementProperty(PropertyType.function),
                 "on_creation": ElementProperty(PropertyType.function),
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
-                _GuiCoreContext._SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
+                __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
                 "multiple": ElementProperty(PropertyType.boolean, False),
+                "filter_by": ElementProperty(_GuiCoreScenarioProperties),
             },
             },
             inner_properties={
             inner_properties={
                 "inner_scenarios": ElementProperty(
                 "inner_scenarios": ElementProperty(
                     PropertyType.lov,
                     PropertyType.lov,
-                    f"{{{__CTX_VAR_NAME}.get_scenarios(<tp:prop:{_GuiCoreContext._SEL_SCENARIOS_PROP}>)}}",
+                    f"{{{__CTX_VAR_NAME}.get_scenarios(<tp:prop:{__SEL_SCENARIOS_PROP}>, "
+                    + f"{__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>)}}",
                 ),
                 ),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR}}}"),
+                "error": ElementProperty(
+                    PropertyType.react, f"{{{__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>}}"
+                ),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "scenario_edit": ElementProperty(
                 "scenario_edit": ElementProperty(
                     _GuiCoreScenarioAdapter,
                     _GuiCoreScenarioAdapter,
-                    f"{{{__CTX_VAR_NAME}.get_scenario_by_id({_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR})}}",
+                    f"{{{__CTX_VAR_NAME}.get_scenario_by_id({__SCENARIO_SELECTOR_ID_VAR}<tp:uniq:sc>)}}",
                 ),
                 ),
                 "on_scenario_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_scenario}}"),
                 "on_scenario_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_scenario}}"),
+                "update_sc_vars": ElementProperty(
+                    PropertyType.string,
+                    f"filter={__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>;"
+                    + f"sc_id={__SCENARIO_SELECTOR_ID_VAR}<tp:uniq:sc>;"
+                    + f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>",
+                ),
             },
             },
         ),
         ),
         "scenario": Element(
         "scenario": Element(
@@ -102,7 +116,13 @@ class _GuiCore(ElementLibrary):
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR}}}"),
+                "error": ElementProperty(
+                    PropertyType.react, f"{{{__SCENARIO_VIZ_ERROR_VAR}<tp:uniq:sv>}}"
+                ),
+                "update_sc_vars": ElementProperty(
+                    PropertyType.string,
+                    f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sv>",
+                ),
             },
             },
         ),
         ),
         "scenario_dag": Element(
         "scenario_dag": Element(
@@ -133,23 +153,23 @@ class _GuiCore(ElementLibrary):
                 "height": ElementProperty(PropertyType.string, "50vh"),
                 "height": ElementProperty(PropertyType.string, "50vh"),
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "show_pins": ElementProperty(PropertyType.boolean, True),
                 "show_pins": ElementProperty(PropertyType.boolean, True),
-                _GuiCoreContext._DATANODE_SEL_SCENARIO_PROP: ElementProperty(PropertyType.dynamic_list),
+                __DATANODE_SEL_SCENARIO_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
                 "multiple": ElementProperty(PropertyType.boolean, False),
             },
             },
             inner_properties={
             inner_properties={
                 "datanodes": ElementProperty(
                 "datanodes": ElementProperty(
                     PropertyType.lov,
                     PropertyType.lov,
-                    f"{{{__CTX_VAR_NAME}.get_datanodes_tree(<tp:prop:{_GuiCoreContext._DATANODE_SEL_SCENARIO_PROP}>)}}",
+                    f"{{{__CTX_VAR_NAME}.get_datanodes_tree(<tp:prop:{__DATANODE_SEL_SCENARIO_PROP}>)}}",
                 ),
                 ),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __DATANODE_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __DATANODE_ADAPTER),
             },
             },
         ),
         ),
         "data_node": Element(
         "data_node": Element(
-            _GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP,
+            __DATANODE_VIZ_DATA_NODE_PROP,
             {
             {
                 "id": ElementProperty(PropertyType.string),
                 "id": ElementProperty(PropertyType.string),
-                _GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP: ElementProperty(_GuiCoreDatanodeAdapter),
+                __DATANODE_VIZ_DATA_NODE_PROP: ElementProperty(_GuiCoreDatanodeAdapter),
                 "active": ElementProperty(PropertyType.dynamic_boolean, True),
                 "active": ElementProperty(PropertyType.dynamic_boolean, True),
                 "expandable": ElementProperty(PropertyType.boolean, True),
                 "expandable": ElementProperty(PropertyType.boolean, True),
                 "expanded": ElementProperty(PropertyType.boolean, True),
                 "expanded": ElementProperty(PropertyType.boolean, True),
@@ -168,43 +188,39 @@ class _GuiCore(ElementLibrary):
             inner_properties={
             inner_properties={
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._DATANODE_VIZ_ERROR_VAR}}}"),
+                "error": ElementProperty(
+                    PropertyType.react, f"{{{__DATANODE_VIZ_ERROR_VAR}<tp:uniq:dn>}}"
+                ),
                 "scenarios": ElementProperty(
                 "scenarios": ElementProperty(
                     PropertyType.lov,
                     PropertyType.lov,
-                    f"{{{__CTX_VAR_NAME}.get_scenarios_for_owner({_GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    f"{{{__CTX_VAR_NAME}.get_scenarios_for_owner({__DATANODE_VIZ_OWNER_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "dn_properties": ElementProperty(
                 "dn_properties": ElementProperty(
                     PropertyType.react,
                     PropertyType.react,
                     f"{{{__CTX_VAR_NAME}.get_data_node_properties("
                     f"{{{__CTX_VAR_NAME}.get_data_node_properties("
-                    + f"{_GuiCoreContext._DATANODE_VIZ_PROPERTIES_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    + f"{__DATANODE_VIZ_PROPERTIES_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "history": ElementProperty(
                 "history": ElementProperty(
                     PropertyType.react,
                     PropertyType.react,
                     f"{{{__CTX_VAR_NAME}.get_data_node_history("
                     f"{{{__CTX_VAR_NAME}.get_data_node_history("
-                    + f"{_GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    + f"{__DATANODE_VIZ_HISTORY_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "tabular_data": ElementProperty(
                 "tabular_data": ElementProperty(
                     PropertyType.data,
                     PropertyType.data,
                     f"{{{__CTX_VAR_NAME}.get_data_node_tabular_data("
                     f"{{{__CTX_VAR_NAME}.get_data_node_tabular_data("
-                    + f"{_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    + f"{__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "tabular_columns": ElementProperty(
                 "tabular_columns": ElementProperty(
                     PropertyType.dynamic_string,
                     PropertyType.dynamic_string,
                     f"{{{__CTX_VAR_NAME}.get_data_node_tabular_columns("
                     f"{{{__CTX_VAR_NAME}.get_data_node_tabular_columns("
-                    + f"{_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    + f"{__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>)}}",
                     with_update=True,
                     with_update=True,
                 ),
                 ),
                 "chart_config": ElementProperty(
                 "chart_config": ElementProperty(
                     PropertyType.dynamic_string,
                     PropertyType.dynamic_string,
                     f"{{{__CTX_VAR_NAME}.get_data_node_chart_config("
                     f"{{{__CTX_VAR_NAME}.get_data_node_chart_config("
-                    + f"{_GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR},"
-                    + "<tp:uniq:dn>)}",
+                    + f"{__DATANODE_VIZ_DATA_CHART_ID_VAR}<tp:uniq:dn>)}}",
                     with_update=True,
                     with_update=True,
                 ),
                 ),
                 "on_data_value": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.update_data}}"),
                 "on_data_value": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.update_data}}"),
@@ -214,11 +230,12 @@ class _GuiCore(ElementLibrary):
                 "on_lock": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.lock_datanode_for_edit}}"),
                 "on_lock": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.lock_datanode_for_edit}}"),
                 "update_dn_vars": ElementProperty(
                 "update_dn_vars": ElementProperty(
                     PropertyType.string,
                     PropertyType.string,
-                    f"data_id={_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR};"
-                    + f"history_id={_GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR};"
-                    + f"owner_id={_GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR};"
-                    + f"chart_id={_GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR};"
-                    + f"properties_id={_GuiCoreContext._DATANODE_VIZ_PROPERTIES_ID_VAR}",
+                    f"data_id={__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>;"
+                    + f"history_id={__DATANODE_VIZ_HISTORY_ID_VAR}<tp:uniq:dn>;"
+                    + f"owner_id={__DATANODE_VIZ_OWNER_ID_VAR}<tp:uniq:dn>;"
+                    + f"chart_id={__DATANODE_VIZ_DATA_CHART_ID_VAR}<tp:uniq:dn>;"
+                    + f"properties_id={__DATANODE_VIZ_PROPERTIES_ID_VAR}<tp:uniq:dn>;"
+                    + f"error_id={__DATANODE_VIZ_ERROR_VAR}<tp:uniq:dn>",
                 ),
                 ),
             },
             },
         ),
         ),
@@ -243,7 +260,12 @@ class _GuiCore(ElementLibrary):
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
                 "on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
-                "error": ElementProperty(PropertyType.dynamic_string, f"{{{_GuiCoreContext._JOB_SELECTOR_ERROR_VAR}}}"),
+                "error": ElementProperty(
+                    PropertyType.dynamic_string, f"{{{__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>}}"
+                ),
+                "update_jb_vars": ElementProperty(
+                    PropertyType.string, f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>"
+                ),
             },
             },
         ),
         ),
     }
     }
@@ -258,17 +280,12 @@ class _GuiCore(ElementLibrary):
         return ["lib/taipy-gui-core.js"]
         return ["lib/taipy-gui-core.js"]
 
 
     def on_init(self, gui: Gui) -> t.Optional[t.Tuple[str, t.Any]]:
     def on_init(self, gui: Gui) -> t.Optional[t.Tuple[str, t.Any]]:
-        gui._get_default_locals_bind().update({v: "" for v in _GuiCore.__INNER_VARS})
         ctx = _GuiCoreContext(gui)
         ctx = _GuiCoreContext(gui)
         gui._add_adapter_for_type(_GuiCore.__SCENARIO_ADAPTER, ctx.scenario_adapter)
         gui._add_adapter_for_type(_GuiCore.__SCENARIO_ADAPTER, ctx.scenario_adapter)
         gui._add_adapter_for_type(_GuiCore.__DATANODE_ADAPTER, ctx.data_node_adapter)
         gui._add_adapter_for_type(_GuiCore.__DATANODE_ADAPTER, ctx.data_node_adapter)
         gui._add_adapter_for_type(_GuiCore.__JOB_ADAPTER, ctx.job_adapter)
         gui._add_adapter_for_type(_GuiCore.__JOB_ADAPTER, ctx.job_adapter)
         return _GuiCore.__CTX_VAR_NAME, ctx
         return _GuiCore.__CTX_VAR_NAME, ctx
 
 
-    def on_user_init(self, state: State):
-        for var in _GuiCore.__INNER_VARS:
-            state._add_attribute(var, "")
-
     def get_version(self) -> str:
     def get_version(self) -> str:
         if not hasattr(self, "version"):
         if not hasattr(self, "version"):
             self.version = _get_version()
             self.version = _get_version()

+ 82 - 26
taipy/gui_core/_adapters.py

@@ -9,10 +9,13 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
+import inspect
+import json
 import math
 import math
 import typing as t
 import typing as t
 from enum import Enum
 from enum import Enum
 from numbers import Number
 from numbers import Number
+from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
 
 
 import pandas as pd
 import pandas as pd
 
 
@@ -167,7 +170,6 @@ class _GuiCoreScenarioNoUpdate(_TaipyBase, _DoNotUpdate):
 
 
 
 
 class _GuiCoreDatanodeAdapter(_TaipyBase):
 class _GuiCoreDatanodeAdapter(_TaipyBase):
-
     @staticmethod
     @staticmethod
     def _is_tabular_data(datanode: DataNode, value: t.Any):
     def _is_tabular_data(datanode: DataNode, value: t.Any):
         if isinstance(datanode, _TabularDataNodeMixin):
         if isinstance(datanode, _TabularDataNodeMixin):
@@ -177,32 +179,31 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
         return False
         return False
 
 
     def __get_data(self, dn: DataNode):
     def __get_data(self, dn: DataNode):
-            if dn._last_edit_date:
-                if isinstance(dn, _TabularDataNodeMixin):
+        if dn._last_edit_date:
+            if isinstance(dn, _TabularDataNodeMixin):
+                return (None, None, True, None)
+            try:
+                value = dn.read()
+                if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
                     return (None, None, True, None)
                     return (None, None, True, None)
-                try:
-                    value = dn.read()
-                    if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
-                        return (None, None, True, None)
-                    val_type = (
-                        "date"
-                        if "date" in type(value).__name__
-                        else type(value).__name__
-                        if isinstance(value, Number)
-                        else None
-                    )
-                    if isinstance(value, float):
-                        if math.isnan(value):
-                            value = None
-                    return (
-                        value,
-                        val_type,
-                        None,
-                        None,
-                    )
-                except Exception as e:
-                    return (None, None, None, f"read data_node: {e}")
-            return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
+                val_type = (
+                    "date"
+                    if "date" in type(value).__name__
+                    else type(value).__name__
+                    if isinstance(value, Number)
+                    else None
+                )
+                if isinstance(value, float) and math.isnan(value):
+                        value = None
+                return (
+                    value,
+                    val_type,
+                    None,
+                    None,
+                )
+            except Exception as e:
+                return (None, None, None, f"read data_node: {e}")
+        return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
 
 
     def get(self):
     def get(self):
         data = super().get()
         data = super().get()
@@ -240,3 +241,58 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
     @staticmethod
     @staticmethod
     def get_hash():
     def get_hash():
         return _TaipyBase._HOLDER_PREFIX + "Dn"
         return _TaipyBase._HOLDER_PREFIX + "Dn"
+
+
+def _attr_filter(attrVal: t.Any):
+    return not inspect.isroutine(attrVal)
+
+
+def _attr_type(attr: str):
+    return "date" if "date" in attr else "boolean" if attr.startswith("is_") else "string"
+
+
+_operators: t.Dict[str, t.Callable] = {
+    "==": eq,
+    "!=": ne,
+    "<": lt,
+    "<=": le,
+    ">": gt,
+    ">=": ge,
+    "contains": contains,
+}
+
+
+def _invoke_action(ent: t.Any, col: str, action: str, val: t.Any) -> bool:
+    try:
+        if op := _operators.get(action):
+            return op(attrgetter(col)(ent), val)
+    except Exception as e:
+        _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
+    return True
+
+
+class _GuiCoreScenarioProperties(_TaipyBase):
+    __SCENARIO_ATTRIBUTES = [a[0] for a in inspect.getmembers(Scenario, _attr_filter) if not a[0].startswith("_")]
+    __DATANODE_ATTRIBUTES = [a[0] for a in inspect.getmembers(DataNode, _attr_filter) if not a[0].startswith("_")]
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "ScP"
+
+    def get(self):
+        data = super().get()
+        if isinstance(data, str):
+            data = data.split(";")
+        if isinstance(data, (list, tuple)):
+            return json.dumps(
+                [
+                    (attr, _attr_type(attr))
+                    for attr in data
+                    if attr
+                    and isinstance(attr, str)
+                    and (parts := attr.split("."))
+                    and (len(parts) > 1 and parts[1] in _GuiCoreScenarioProperties.__DATANODE_ATTRIBUTES)
+                    or attr in _GuiCoreScenarioProperties.__SCENARIO_ATTRIBUTES
+                ]
+            )
+        return None

+ 155 - 103
taipy/gui_core/_context.py

@@ -12,6 +12,7 @@
 import json
 import json
 import typing as t
 import typing as t
 from collections import defaultdict
 from collections import defaultdict
+from datetime import datetime
 from numbers import Number
 from numbers import Number
 from threading import Lock
 from threading import Lock
 
 
@@ -59,7 +60,7 @@ from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.gui import _DoNotUpdate
 
 
-from ._adapters import _EntityType, _GuiCoreDatanodeAdapter
+from ._adapters import _attr_type, _EntityType, _GuiCoreDatanodeAdapter, _invoke_action
 
 
 
 
 class _GuiCoreContext(CoreEventConsumerBase):
 class _GuiCoreContext(CoreEventConsumerBase):
@@ -73,19 +74,6 @@ class _GuiCoreContext(CoreEventConsumerBase):
     __ENTITY_PROPS = (__PROP_CONFIG_ID, __PROP_DATE, __PROP_ENTITY_NAME)
     __ENTITY_PROPS = (__PROP_CONFIG_ID, __PROP_DATE, __PROP_ENTITY_NAME)
     __ACTION = "action"
     __ACTION = "action"
     _CORE_CHANGED_NAME = "core_changed"
     _CORE_CHANGED_NAME = "core_changed"
-    _SCENARIO_SELECTOR_ERROR_VAR = "gui_core_sc_error"
-    _SCENARIO_SELECTOR_ID_VAR = "gui_core_sc_id"
-    _SCENARIO_VIZ_ERROR_VAR = "gui_core_sv_error"
-    _JOB_SELECTOR_ERROR_VAR = "gui_core_js_error"
-    _DATANODE_VIZ_ERROR_VAR = "gui_core_dv_error"
-    _DATANODE_VIZ_OWNER_ID_VAR = "gui_core_dv_owner_id"
-    _DATANODE_VIZ_HISTORY_ID_VAR = "gui_core_dv_history_id"
-    _DATANODE_VIZ_PROPERTIES_ID_VAR = "gui_core_dv_properties_id"
-    _DATANODE_VIZ_DATA_ID_VAR = "gui_core_dv_data_id"
-    _DATANODE_VIZ_DATA_CHART_ID_VAR = "gui_core_dv_data_chart_id"
-    _DATANODE_VIZ_DATA_NODE_PROP = "data_node"
-    _DATANODE_SEL_SCENARIO_PROP = "scenario"
-    _SEL_SCENARIOS_PROP = "scenarios"
 
 
     def __init__(self, gui: Gui) -> None:
     def __init__(self, gui: Gui) -> None:
         self.gui = gui
         self.gui = gui
@@ -206,61 +194,113 @@ class _GuiCoreContext(CoreEventConsumerBase):
         finally:
         finally:
             self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"jobs": True})
             self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"jobs": True})
 
 
-    def scenario_adapter(self, scenario_or_cycle):
+    def no_change_adapter(self, entity: t.List):
+        return entity
+
+    def cycle_adapter(self, cycle: Cycle):
         try:
         try:
             if (
             if (
-                hasattr(scenario_or_cycle, "id")
-                and is_readable(scenario_or_cycle.id)
-                and core_get(scenario_or_cycle.id) is not None
+                isinstance(cycle, Cycle)
+                and is_readable(cycle.id)
+                and core_get(cycle.id) is not None
+                and self.scenario_by_cycle
             ):
             ):
-                if self.scenario_by_cycle and isinstance(scenario_or_cycle, Cycle):
-                    return (
-                        scenario_or_cycle.id,
-                        scenario_or_cycle.get_simple_label(),
-                        sorted(
-                            self.scenario_by_cycle.get(scenario_or_cycle, []),
-                            key=_GuiCoreContext.get_entity_creation_date_iso,
-                        ),
-                        _EntityType.CYCLE.value,
-                        False,
-                    )
-                elif isinstance(scenario_or_cycle, Scenario):
-                    return (
-                        scenario_or_cycle.id,
-                        scenario_or_cycle.get_simple_label(),
-                        None,
-                        _EntityType.SCENARIO.value,
-                        scenario_or_cycle.is_primary,
-                    )
+                return [
+                    cycle.id,
+                    cycle.get_simple_label(),
+                    sorted(
+                        self.scenario_by_cycle.get(cycle, []),
+                        key=_GuiCoreContext.get_entity_creation_date_iso,
+                    ),
+                    _EntityType.CYCLE.value,
+                    False,
+                ]
+        except Exception as e:
+            _warn(
+                f"Access to {type(cycle).__name__} " + f"({cycle.id if hasattr(cycle, 'id') else 'No_id'})" + " failed",
+                e,
+            )
+        return None
+
+    def scenario_adapter(self, scenario: Scenario):
+        if isinstance(scenario, (tuple, list)):
+            return scenario
+        try:
+            if isinstance(scenario, Scenario) and is_readable(scenario.id) and core_get(scenario.id) is not None:
+                return [
+                    scenario.id,
+                    scenario.get_simple_label(),
+                    None,
+                    _EntityType.SCENARIO.value,
+                    scenario.is_primary,
+                ]
         except Exception as e:
         except Exception as e:
             _warn(
             _warn(
-                f"Access to {type(scenario_or_cycle)} "
-                + f"({scenario_or_cycle.id if hasattr(scenario_or_cycle, 'id') else 'No_id'})"
+                f"Access to {type(scenario).__name__} "
+                + f"({scenario.id if hasattr(scenario, 'id') else 'No_id'})"
                 + " failed",
                 + " failed",
                 e,
                 e,
             )
             )
         return None
         return None
 
 
-    def get_scenarios(self, scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]]):
+    def filter_scenarios(self, cycle: t.List, col: str, action: str, val: t.Any):
+        cycle[2] = [e for e in cycle[2] if _invoke_action(e, col, action, val)]
+        return cycle
+
+    def adapt_scenarios(self, cycle: t.List):
+        cycle[2] = [self.scenario_adapter(e) for e in cycle[2]]
+        return cycle
+
+    def get_scenarios(
+        self, scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]], filters: t.Optional[t.List[t.Dict[str, t.Any]]]
+    ):
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
-        if scenarios is None:
-            with self.lock:
-                if self.scenario_by_cycle is None:
-                    self.scenario_by_cycle = get_cycles_scenarios()
+        with self.lock:
+            if self.scenario_by_cycle is None:
+                self.scenario_by_cycle = get_cycles_scenarios()
+            if scenarios is None:
                 for cycle, c_scenarios in self.scenario_by_cycle.items():
                 for cycle, c_scenarios in self.scenario_by_cycle.items():
                     if cycle is None:
                     if cycle is None:
                         cycles_scenarios.extend(c_scenarios)
                         cycles_scenarios.extend(c_scenarios)
                     else:
                     else:
                         cycles_scenarios.append(cycle)
                         cycles_scenarios.append(cycle)
-        else:
+        if scenarios is not None:
             cycles_scenarios = scenarios
             cycles_scenarios = scenarios
-        return sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_creation_date_iso)
+        # sorting
+        adapted_list = [
+            self.cycle_adapter(e) if isinstance(e, Cycle) else e
+            for e in sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_creation_date_iso)
+        ]
+        if filters:
+            # filtering
+            filtered_list = list(adapted_list)
+            for fd in filters:
+                col = fd.get("col", "")
+                col_type = _attr_type(col)
+                val = fd.get("value")
+                action = fd.get("action", "")
+                if isinstance(val, str) and col_type == "date":
+                    val = datetime.fromisoformat(val[:-1])
+                # level 1 filtering
+                filtered_list = [
+                    e for e in filtered_list if not isinstance(e, Scenario) or _invoke_action(e, col, action, val)
+                ]
+                # level 2 filtering
+                filtered_list = [
+                    self.filter_scenarios(e, col, action, val) if not isinstance(e, Scenario) else e
+                    for e in filtered_list
+                ]
+            # remove empty cycles
+            adapted_list = [
+                e for e in filtered_list if isinstance(e, Scenario) or (isinstance(e, (tuple, list)) and len(e[2]))
+            ]
+        return adapted_list
 
 
     def select_scenario(self, state: State, id: str, payload: t.Dict[str, str]):
     def select_scenario(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         args = payload.get("args")
-        if args is None or not isinstance(args, list) or len(args) == 0:
+        if args is None or not isinstance(args, list) or len(args) < 2:
             return
             return
-        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, args[0])
+        state.assign(args[0], args[1])
 
 
     def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
     def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
         if not id or not is_readable(t.cast(ScenarioId, id)):
         if not id or not is_readable(t.cast(ScenarioId, id)):
@@ -290,6 +330,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             or not isinstance(args[start_idx + 2], dict)
             or not isinstance(args[start_idx + 2], dict)
         ):
         ):
             return
             return
+        error_var = payload.get("error_id")
         update = args[start_idx]
         update = args[start_idx]
         delete = args[start_idx + 1]
         delete = args[start_idx + 1]
         data = args[start_idx + 2]
         data = args[start_idx + 2]
@@ -301,18 +342,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             if delete:
             if delete:
                 if not is_deletable(scenario_id):
                 if not is_deletable(scenario_id):
-                    state.assign(
-                        _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario. {scenario_id} is not deletable."
-                    )
+                    state.assign(error_var, f"Scenario. {scenario_id} is not deletable.")
                     return
                     return
                 try:
                 try:
                     core_delete(scenario_id)
                     core_delete(scenario_id)
                 except Exception as e:
                 except Exception as e:
-                    state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error deleting Scenario. {e}")
+                    state.assign(error_var, f"Error deleting Scenario. {e}")
             else:
             else:
-                if not self.__check_readable_editable(
-                    state, scenario_id, "Scenario", _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR
-                ):
+                if not self.__check_readable_editable(state, scenario_id, "Scenario", error_var):
                     return
                     return
                 scenario = core_get(scenario_id)
                 scenario = core_get(scenario_id)
         else:
         else:
@@ -320,15 +357,13 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
                 config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
                 scenario_config = Config.scenarios.get(config_id)
                 scenario_config = Config.scenarios.get(config_id)
                 if with_dialog and scenario_config is None:
                 if with_dialog and scenario_config is None:
-                    state.assign(
-                        _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})"
-                    )
+                    state.assign(error_var, f"Invalid configuration id ({config_id})")
                     return
                     return
                 date_str = data.get(_GuiCoreContext.__PROP_DATE)
                 date_str = data.get(_GuiCoreContext.__PROP_DATE)
                 try:
                 try:
                     date = parser.parse(date_str) if isinstance(date_str, str) else None
                     date = parser.parse(date_str) if isinstance(date_str, str) else None
                 except Exception as e:
                 except Exception as e:
-                    state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
+                    state.assign(error_var, f"Invalid date ({date_str}).{e}")
                     return
                     return
             else:
             else:
                 scenario_config = None
                 scenario_config = None
@@ -356,17 +391,17 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         if isinstance(res, Scenario):
                         if isinstance(res, Scenario):
                             # everything's fine
                             # everything's fine
                             scenario_id = res.id
                             scenario_id = res.id
-                            state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
+                            state.assign(error_var, "")
                             return
                             return
                         if res:
                         if res:
                             # do not create
                             # do not create
-                            state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"{res}")
+                            state.assign(error_var, f"{res}")
                             return
                             return
                     except Exception as e:  # pragma: no cover
                     except Exception as e:  # pragma: no cover
                         if not gui._call_on_exception(on_creation, e):
                         if not gui._call_on_exception(on_creation, e):
                             _warn(f"on_creation(): Exception raised in '{on_creation}()'", e)
                             _warn(f"on_creation(): Exception raised in '{on_creation}()'", e)
                         state.assign(
                         state.assign(
-                            _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
+                            error_var,
                             f"Error creating Scenario with '{on_creation}()'. {e}",
                             f"Error creating Scenario with '{on_creation}()'. {e}",
                         )
                         )
                         return
                         return
@@ -377,7 +412,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         scenario_config = next(sc for k, sc in Config.scenarios.items() if k != "default")
                         scenario_config = next(sc for k, sc in Config.scenarios.items() if k != "default")
                     else:
                     else:
                         state.assign(
                         state.assign(
-                            _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
+                            error_var,
                             "Error creating Scenario: only one scenario config needed "
                             "Error creating Scenario: only one scenario config needed "
                             + f"({len(Config.scenarios) - 1}) found.",
                             + f"({len(Config.scenarios) - 1}) found.",
                         )
                         )
@@ -386,7 +421,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 scenario = create_scenario(scenario_config, date, name)
                 scenario = create_scenario(scenario_config, date, name)
                 scenario_id = scenario.id
                 scenario_id = scenario.id
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
+                state.assign(error_var, f"Error creating Scenario. {e}")
             finally:
             finally:
                 self.scenario_refresh(scenario_id)
                 self.scenario_refresh(scenario_id)
                 if scenario and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
                 if scenario and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
@@ -397,9 +432,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         _warn("Can't find value variable name in context", e)
                         _warn("Can't find value variable name in context", e)
         if scenario:
         if scenario:
             if not is_editable(scenario):
             if not is_editable(scenario):
-                state.assign(
-                    _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Scenario {scenario_id or name} is not editable."
-                )
+                state.assign(error_var, f"Scenario {scenario_id or name} is not editable.")
                 return
                 return
             with scenario as sc:
             with scenario as sc:
                 sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
                 sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
@@ -413,18 +446,24 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             key = prop.get("key")
                             key = prop.get("key")
                             if key and key not in _GuiCoreContext.__ENTITY_PROPS:
                             if key and key not in _GuiCoreContext.__ENTITY_PROPS:
                                 sc._properties[key] = prop.get("value")
                                 sc._properties[key] = prop.get("value")
-                        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
+                        state.assign(error_var, "")
                     except Exception as e:
                     except Exception as e:
-                        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
+                        state.assign(error_var, f"Error creating Scenario. {e}")
+
+    @staticmethod
+    def __assign_var(state: State, var_name: t.Optional[str], msg: str):
+        if var_name:
+            state.assign(var_name, msg)
 
 
     def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
     def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
             return
+        error_var = payload.get("error_id")
         data = args[0]
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         sequence = data.get("sequence")
         sequence = data.get("sequence")
-        if not self.__check_readable_editable(state, entity_id, "Scenario", _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR):
+        if not self.__check_readable_editable(state, entity_id, "Scenario", error_var):
             return
             return
         scenario: Scenario = core_get(entity_id)
         scenario: Scenario = core_get(entity_id)
         if scenario:
         if scenario:
@@ -436,8 +475,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                         primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                         if primary is True:
                         if primary is True:
                             if not is_promotable(scenario):
                             if not is_promotable(scenario):
-                                state.assign(
-                                    _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Scenario {entity_id} is not promotable."
+                                _GuiCoreContext.__assign_var(
+                                    state, error_var, f"Scenario {entity_id} is not promotable."
                                 )
                                 )
                                 return
                                 return
                             set_primary(scenario)
                             set_primary(scenario)
@@ -453,21 +492,23 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             seqEntity.tasks = data.get("task_ids")
                             seqEntity.tasks = data.get("task_ids")
                             self.__edit_properties(seqEntity, data)
                             self.__edit_properties(seqEntity, data)
                         else:
                         else:
-                            state.assign(
-                                _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
+                            _GuiCoreContext.__assign_var(
+                                state,
+                                error_var,
                                 f"Sequence {name} is not available in Scenario {entity_id}.",
                                 f"Sequence {name} is not available in Scenario {entity_id}.",
                             )
                             )
                             return
                             return
 
 
-                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
+                _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating {type(scenario).__name__}. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating {type(scenario).__name__}. {e}")
 
 
     def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
     def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
             return
         data = args[0]
         data = args[0]
+        error_var = payload.get("error_id")
         try:
         try:
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             entity = core_get(scenario_id)
             entity = core_get(scenario_id)
@@ -475,8 +516,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 entity = entity.sequences.get(sequence)
                 entity = entity.sequences.get(sequence)
 
 
             if not is_submittable(entity):
             if not is_submittable(entity):
-                state.assign(
-                    _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
+                _GuiCoreContext.__assign_var(
+                    state,
+                    error_var,
                     f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable.",
                     f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable.",
                 )
                 )
                 return
                 return
@@ -495,9 +537,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         with self.submissions_lock:
                         with self.submissions_lock:
                             self.client_submission[submission_entity.id] = SubmissionStatus.SUBMITTED
                             self.client_submission[submission_entity.id] = SubmissionStatus.SUBMITTED
                         self.submission_status_callback(submission_entity.id)
                         self.submission_status_callback(submission_entity.id)
-                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
+                _GuiCoreContext.__assign_var(state, error_var, "")
         except Exception as e:
         except Exception as e:
-            state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
+            _GuiCoreContext.__assign_var(state, error_var, f"Error submitting entity. {e}")
 
 
     def __do_datanodes_tree(self):
     def __do_datanodes_tree(self):
         if self.data_nodes_by_owner is None:
         if self.data_nodes_by_owner is None:
@@ -509,7 +551,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
         with self.lock:
         with self.lock:
             self.__do_datanodes_tree()
             self.__do_datanodes_tree()
         if scenarios is None:
         if scenarios is None:
-            return (self.data_nodes_by_owner.get(None) if self.data_nodes_by_owner else []) + self.get_scenarios(None)
+            return (self.data_nodes_by_owner.get(None) if self.data_nodes_by_owner else []) + self.get_scenarios(
+                None, None
+            )
         if not self.data_nodes_by_owner:
         if not self.data_nodes_by_owner:
             return []
             return []
         if isinstance(scenarios, (list, tuple)) and len(scenarios) > 1:
         if isinstance(scenarios, (list, tuple)) and len(scenarios) > 1:
@@ -518,6 +562,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return [d for owner in owners for d in t.cast(list, self.data_nodes_by_owner.get(owner.id))]
         return [d for owner in owners for d in t.cast(list, self.data_nodes_by_owner.get(owner.id))]
 
 
     def data_node_adapter(self, data):
     def data_node_adapter(self, data):
+        if isinstance(data, (tuple, list)):
+            return data
         try:
         try:
             if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
             if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
                 if isinstance(data, DataNode):
                 if isinstance(data, DataNode):
@@ -620,31 +666,33 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cancel_job(job_id)
                         cancel_job(job_id)
                     except Exception as e:
                     except Exception as e:
                         errs.append(f"Error canceling job. {e}")
                         errs.append(f"Error canceling job. {e}")
-            state.assign(_GuiCoreContext._JOB_SELECTOR_ERROR_VAR, "<br/>".join(errs) if errs else "")
+            _GuiCoreContext.__assign_var(state, payload.get("error_id"), "<br/>".join(errs) if errs else "")
 
 
     def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
     def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
             return
+        error_var = payload.get("error_id")
         data = args[0]
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+        if not self.__check_readable_editable(state, entity_id, "DataNode", error_var):
             return
             return
         entity: DataNode = core_get(entity_id)
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
         if isinstance(entity, DataNode):
             try:
             try:
                 self.__edit_properties(entity, data)
                 self.__edit_properties(entity, data)
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+                _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode. {e}")
 
 
     def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str]):
     def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
             return
         data = args[0]
         data = args[0]
+        error_var = payload.get("error_id")
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(state, entity_id, "Datanode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+        if not self.__check_readable_editable(state, entity_id, "Datanode", error_var):
             return
             return
         lock = data.get("lock", True)
         lock = data.get("lock", True)
         entity: DataNode = core_get(entity_id)
         entity: DataNode = core_get(entity_id)
@@ -654,9 +702,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     entity.lock_edit(self.gui._get_client_id())
                     entity.lock_edit(self.gui._get_client_id())
                 else:
                 else:
                     entity.unlock_edit(self.gui._get_client_id())
                     entity.unlock_edit(self.gui._get_client_id())
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+                _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error locking Datanode. {e}")
+                _GuiCoreContext.__assign_var(state, error_var, f"Error locking Datanode. {e}")
 
 
     def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: t.Dict[str, str]):
     def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: t.Dict[str, str]):
         with entity as ent:
         with entity as ent:
@@ -731,12 +779,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return sorted(res, key=lambda r: r[0], reverse=True)
             return sorted(res, key=lambda r: r[0], reverse=True)
         return _DoNotUpdate()
         return _DoNotUpdate()
 
 
-    def __check_readable_editable(self, state: State, id: str, ent_type: str, var: str):
+    def __check_readable_editable(self, state: State, id: str, ent_type: str, var: t.Optional[str]):
         if not is_readable(t.cast(ScenarioId, id)):
         if not is_readable(t.cast(ScenarioId, id)):
-            state.assign(var, f"{ent_type} {id} is not readable.")
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable.")
             return False
             return False
         if not is_editable(t.cast(ScenarioId, id)):
         if not is_editable(t.cast(ScenarioId, id)):
-            state.assign(var, f"{ent_type} {id} is not editable.")
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable.")
             return False
             return False
         return True
         return True
 
 
@@ -745,8 +793,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
             return
         data = args[0]
         data = args[0]
+        error_var = payload.get("error_id")
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(state, entity_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+        if not self.__check_readable_editable(state, entity_id, "DataNode", error_var):
             return
             return
         entity: DataNode = core_get(entity_id)
         entity: DataNode = core_get(entity_id)
         if isinstance(entity, DataNode):
         if isinstance(entity, DataNode):
@@ -762,15 +811,16 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     comment=data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT),
                     comment=data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT),
                 )
                 )
                 entity.unlock_edit(self.gui._get_client_id())
                 entity.unlock_edit(self.gui._get_client_id())
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+                _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode value. {e}")
-            state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, entity_id)  # this will update the data value
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode 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):
     def tabular_data_edit(self, state: State, var_name: str, payload: dict):
+        error_var = payload.get("error_id")
         user_data = payload.get("user_data", {})
         user_data = payload.get("user_data", {})
         dn_id = user_data.get("dn_id")
         dn_id = user_data.get("dn_id")
-        if not self.__check_readable_editable(state, dn_id, "DataNode", _GuiCoreContext._DATANODE_VIZ_ERROR_VAR):
+        if not self.__check_readable_editable(state, dn_id, "DataNode", error_var):
             return
             return
         datanode = core_get(dn_id) if dn_id else None
         datanode = core_get(dn_id) if dn_id else None
         if isinstance(datanode, DataNode):
         if isinstance(datanode, DataNode):
@@ -812,23 +862,25 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             data[idx] = val
                             data[idx] = val
                             new_data = data
                             new_data = data
                         else:
                         else:
-                            state.assign(
-                                _GuiCoreContext._DATANODE_VIZ_ERROR_VAR,
+                            _GuiCoreContext.__assign_var(
+                                state,
+                                error_var,
                                 "Error updating Datanode: cannot handle multi-column list value.",
                                 "Error updating Datanode: cannot handle multi-column list value.",
                             )
                             )
                         if data_tuple and new_data is not None:
                         if data_tuple and new_data is not None:
                             new_data = tuple(new_data)
                             new_data = tuple(new_data)
                     else:
                     else:
-                        state.assign(
-                            _GuiCoreContext._DATANODE_VIZ_ERROR_VAR,
+                        _GuiCoreContext.__assign_var(
+                            state,
+                            error_var,
                             "Error updating Datanode tabular value: type does not support at[] indexer.",
                             "Error updating Datanode tabular value: type does not support at[] indexer.",
                         )
                         )
                 if new_data is not None:
                 if new_data is not None:
                     datanode.write(new_data, comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT))
                     datanode.write(new_data, comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT))
-                    state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+                    _GuiCoreContext.__assign_var(state, error_var, "")
             except Exception as e:
             except Exception as e:
-                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode tabular value. {e}")
-        setattr(state, _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, dn_id)
+                _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode tabular value. {e}")
+        _GuiCoreContext.__assign_var(state, payload.get("data_id"), dn_id)
 
 
     def get_data_node_properties(self, id: str, uid: str):
     def get_data_node_properties(self, id: str, uid: str):
         if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
         if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):

+ 5 - 0
taipy/gui_core/viselements.json

@@ -97,6 +97,11 @@
                         "type": "bool",
                         "type": "bool",
                         "default_value": "False",
                         "default_value": "False",
                         "doc": "TODO: If True, the user can select multiple scenarios."
                         "doc": "TODO: If True, the user can select multiple scenarios."
+                    },
+                    {
+                        "name": "filter_by",
+                        "type": "str|list[str]",
+                        "doc": "TODO: a list of scenario attributes to filter on."
                     }
                     }
                 ]
                 ]
             }
             }

+ 15 - 0
tests/core/data/test_data_node.py

@@ -75,6 +75,21 @@ class TestDataNode:
         assert not dn.is_ready_for_reading
         assert not dn.is_ready_for_reading
         assert len(dn.properties) == 0
         assert len(dn.properties) == 0
 
 
+    def test_is_up_to_date_when_not_written(self):
+        dn_confg_1 = Config.configure_in_memory_data_node("dn_1", default_data="a")
+        dn_confg_2 = Config.configure_in_memory_data_node("dn_2")
+        task_config_1 = Config.configure_task("t1", funct_a_b, [dn_confg_1], [dn_confg_2])
+        scenario_config = Config.configure_scenario("sc", [task_config_1])
+
+        scenario = tp.create_scenario(scenario_config)
+
+        assert scenario.dn_1.is_up_to_date is True
+        assert scenario.dn_2.is_up_to_date is False
+
+        tp.submit(scenario)
+        assert scenario.dn_1.is_up_to_date is True
+        assert scenario.dn_2.is_up_to_date is True
+
     def test_create(self):
     def test_create(self):
         a_date = datetime.now()
         a_date = datetime.now()
         dn = DataNode(
         dn = DataNode(

+ 80 - 52
tests/core/test_taipy/test_export.py

@@ -10,21 +10,23 @@
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
 import os
 import os
-import shutil
+import zipfile
 
 
 import pandas as pd
 import pandas as pd
 import pytest
 import pytest
 
 
 import taipy.core.taipy as tp
 import taipy.core.taipy as tp
 from taipy import Config, Frequency, Scope
 from taipy import Config, Frequency, Scope
-from taipy.core.exceptions import ExportFolderAlreadyExists, InvalidExportPath
+from taipy.core.exceptions import ExportPathAlreadyExists
 
 
 
 
 @pytest.fixture(scope="function", autouse=True)
 @pytest.fixture(scope="function", autouse=True)
-def clean_tmp_folder():
-    shutil.rmtree("./tmp", ignore_errors=True)
+def clean_export_zip_file():
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
     yield
     yield
-    shutil.rmtree("./tmp", ignore_errors=True)
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
 
 
 
 
 def plus_1(x):
 def plus_1(x):
@@ -57,15 +59,28 @@ def configure_test_scenario(input_data, frequency=None):
     return scenario_cfg
     return scenario_cfg
 
 
 
 
-def test_export_scenario_to_the_storage_folder():
+def test_export_scenario_with_and_without_zip_extension(tmp_path):
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
+    tp.submit(scenario)
+
+    # Export without the .zip extension should create the tmp.zip file
+    tp.export_scenario(scenario.id, f"{tmp_path}/tmp")
+    assert os.path.exists(f"{tmp_path}/tmp.zip")
+
+    os.remove(f"{tmp_path}/tmp.zip")
 
 
-    with pytest.raises(InvalidExportPath):
-        tp.export_scenario(scenario.id, Config.core.taipy_storage_folder)
+    # Export with the .zip extension should also create the tmp.zip file
+    tp.export_scenario(scenario.id, f"{tmp_path}/tmp.zip")
+    assert os.path.exists(f"{tmp_path}/tmp.zip")
 
 
+    # Export with another extension should create the tmp.<extension>.zip file
+    tp.export_scenario(scenario.id, f"{tmp_path}/tmp.tar.gz")
+    assert os.path.exists(f"{tmp_path}/tmp.tar.gz.zip")
 
 
-def test_export_scenario_with_cycle():
+
+def test_export_scenario_with_cycle(tmp_path):
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
 
 
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
@@ -73,9 +88,11 @@ def test_export_scenario_with_cycle():
     jobs = submission.jobs
     jobs = submission.jobs
 
 
     # Export the submitted scenario
     # Export the submitted scenario
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path)
 
 
-    assert sorted(os.listdir("./tmp/exp_scenario/data_nodes")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/data_nodes")) == sorted(
         [
         [
             f"{scenario.i_1.id}.json",
             f"{scenario.i_1.id}.json",
             f"{scenario.o_1_csv.id}.json",
             f"{scenario.o_1_csv.id}.json",
@@ -84,7 +101,7 @@ def test_export_scenario_with_cycle():
             f"{scenario.o_1_json.id}.json",
             f"{scenario.o_1_json.id}.json",
         ]
         ]
     )
     )
-    assert sorted(os.listdir("./tmp/exp_scenario/tasks")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/tasks")) == sorted(
         [
         [
             f"{scenario.t_1_csv.id}.json",
             f"{scenario.t_1_csv.id}.json",
             f"{scenario.t_1_excel.id}.json",
             f"{scenario.t_1_excel.id}.json",
@@ -92,32 +109,34 @@ def test_export_scenario_with_cycle():
             f"{scenario.t_1_json.id}.json",
             f"{scenario.t_1_json.id}.json",
         ]
         ]
     )
     )
-    assert sorted(os.listdir("./tmp/exp_scenario/scenarios")) == sorted([f"{scenario.id}.json"])
-    assert sorted(os.listdir("./tmp/exp_scenario/jobs")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/scenarios")) == sorted([f"{scenario.id}.json"])
+    assert sorted(os.listdir(f"{tmp_path}/jobs")) == sorted(
         [f"{jobs[0].id}.json", f"{jobs[1].id}.json", f"{jobs[2].id}.json", f"{jobs[3].id}.json"]
         [f"{jobs[0].id}.json", f"{jobs[1].id}.json", f"{jobs[2].id}.json", f"{jobs[3].id}.json"]
     )
     )
-    assert os.listdir("./tmp/exp_scenario/submission") == [f"{submission.id}.json"]
-    assert sorted(os.listdir("./tmp/exp_scenario/cycles")) == sorted([f"{scenario.cycle.id}.json"])
+    assert os.listdir(f"{tmp_path}/submission") == [f"{submission.id}.json"]
+    assert sorted(os.listdir(f"{tmp_path}/cycles")) == sorted([f"{scenario.cycle.id}.json"])
 
 
 
 
-def test_export_scenario_without_cycle():
+def test_export_scenario_without_cycle(tmp_path):
     scenario_cfg = configure_test_scenario(1)
     scenario_cfg = configure_test_scenario(1)
 
 
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export the submitted scenario
     # Export the submitted scenario
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path)
 
 
-    assert os.path.exists("./tmp/exp_scenario/data_nodes")
-    assert os.path.exists("./tmp/exp_scenario/tasks")
-    assert os.path.exists("./tmp/exp_scenario/scenarios")
-    assert os.path.exists("./tmp/exp_scenario/jobs")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    assert not os.path.exists("./tmp/exp_scenario/cycles")  # No cycle
+    assert os.path.exists(f"{tmp_path}/data_nodes")
+    assert os.path.exists(f"{tmp_path}/tasks")
+    assert os.path.exists(f"{tmp_path}/scenarios")
+    assert os.path.exists(f"{tmp_path}/jobs")
+    assert os.path.exists(f"{tmp_path}/submission")
+    assert not os.path.exists(f"{tmp_path}/cycles")  # No cycle
 
 
 
 
-def test_export_scenario_override_existing_files():
+def test_export_scenario_override_existing_files(tmp_path):
     scenario_1_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_1_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_2_cfg = configure_test_scenario(2)
     scenario_2_cfg = configure_test_scenario(2)
 
 
@@ -125,45 +144,54 @@ def test_export_scenario_override_existing_files():
     tp.submit(scenario_1)
     tp.submit(scenario_1)
 
 
     # Export the submitted scenario_1
     # Export the submitted scenario_1
-    tp.export_scenario(scenario_1.id, "./tmp/exp_scenario")
-    assert os.path.exists("./tmp/exp_scenario/data_nodes")
-    assert os.path.exists("./tmp/exp_scenario/tasks")
-    assert os.path.exists("./tmp/exp_scenario/scenarios")
-    assert os.path.exists("./tmp/exp_scenario/jobs")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    assert os.path.exists("./tmp/exp_scenario/cycles")
+    tp.export_scenario(scenario_1.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_1")
+    assert os.path.exists(f"{tmp_path}/scenario_1/data_nodes")
+    assert os.path.exists(f"{tmp_path}/scenario_1/tasks")
+    assert os.path.exists(f"{tmp_path}/scenario_1/scenarios")
+    assert os.path.exists(f"{tmp_path}/scenario_1/jobs")
+    assert os.path.exists(f"{tmp_path}/scenario_1/submission")
+    assert os.path.exists(f"{tmp_path}/scenario_1/cycles")
 
 
     scenario_2 = tp.create_scenario(scenario_2_cfg)
     scenario_2 = tp.create_scenario(scenario_2_cfg)
     tp.submit(scenario_2)
     tp.submit(scenario_2)
 
 
-    # Export the submitted scenario_2 to the same folder should raise an error
-    with pytest.raises(ExportFolderAlreadyExists):
-        tp.export_scenario(scenario_2.id, "./tmp/exp_scenario")
+    # Export the submitted scenario_2 to the same path should raise an error
+    with pytest.raises(ExportPathAlreadyExists):
+        tp.export_scenario(scenario_2.id, "tmp.zip")
 
 
     # Export the submitted scenario_2 without a cycle and override the existing files
     # Export the submitted scenario_2 without a cycle and override the existing files
-    tp.export_scenario(scenario_2.id, "./tmp/exp_scenario", override=True)
-    assert os.path.exists("./tmp/exp_scenario/data_nodes")
-    assert os.path.exists("./tmp/exp_scenario/tasks")
-    assert os.path.exists("./tmp/exp_scenario/scenarios")
-    assert os.path.exists("./tmp/exp_scenario/jobs")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    # The cycles folder should be removed when overriding
-    assert not os.path.exists("./tmp/exp_scenario/cycles")
-
-
-def test_export_scenario_filesystem_with_data():
+    tp.export_scenario(scenario_2.id, "tmp.zip", override=True)
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_2")
+    assert os.path.exists(f"{tmp_path}/scenario_2/data_nodes")
+    assert os.path.exists(f"{tmp_path}/scenario_2/tasks")
+    assert os.path.exists(f"{tmp_path}/scenario_2/scenarios")
+    assert os.path.exists(f"{tmp_path}/scenario_2/jobs")
+    assert os.path.exists(f"{tmp_path}/scenario_2/submission")
+    # The cycles folder should not exists since the new scenario does not have a cycle
+    assert not os.path.exists(f"{tmp_path}/scenario_2/cycles")
+
+
+def test_export_scenario_filesystem_with_data(tmp_path):
     scenario_cfg = configure_test_scenario(1)
     scenario_cfg = configure_test_scenario(1)
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export scenario without data
     # Export scenario without data
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
-    assert not os.path.exists("./tmp/exp_scenario/user_data")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_without_data")
+    assert not os.path.exists(f"{tmp_path}/scenario_without_data/user_data")
 
 
     # Export scenario with data
     # Export scenario with data
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario", include_data=True, override=True)
-    assert os.path.exists("./tmp/exp_scenario/user_data")
-    data_files = [f for _, _, files in os.walk("./tmp/exp_scenario/user_data") for f in files]
+    tp.export_scenario(scenario.id, "tmp.zip", include_data=True, override=True)
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_with_data")
+    assert os.path.exists(f"{tmp_path}/scenario_with_data/user_data")
+
+    data_files = [f for _, _, files in os.walk(f"{tmp_path}/scenario_with_data/user_data") for f in files]
     assert sorted(data_files) == sorted(
     assert sorted(data_files) == sorted(
         [
         [
             f"{scenario.i_1.id}.p",
             f"{scenario.i_1.id}.p",
@@ -188,6 +216,6 @@ def test_export_non_file_based_data_node_raise_warning(caplog):
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export scenario with in-memory data node
     # Export scenario with in-memory data node
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario", include_data=True)
+    tp.export_scenario(scenario.id, "tmp.zip", include_data=True)
     expected_warning = f"Data node {scenario.o_mem.id} is not a file-based data node and the data will not be exported"
     expected_warning = f"Data node {scenario.o_mem.id} is not a file-based data node and the data will not be exported"
     assert expected_warning in caplog.text
     assert expected_warning in caplog.text

+ 63 - 56
tests/core/test_taipy/test_export_with_sql_repo.py

@@ -10,21 +10,23 @@
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
 import os
 import os
-import shutil
+import zipfile
 
 
 import pandas as pd
 import pandas as pd
 import pytest
 import pytest
 
 
 import taipy.core.taipy as tp
 import taipy.core.taipy as tp
 from taipy import Config, Frequency, Scope
 from taipy import Config, Frequency, Scope
-from taipy.core.exceptions import ExportFolderAlreadyExists, InvalidExportPath
+from taipy.core.exceptions import ExportPathAlreadyExists
 
 
 
 
 @pytest.fixture(scope="function", autouse=True)
 @pytest.fixture(scope="function", autouse=True)
-def clean_tmp_folder():
-    shutil.rmtree("./tmp", ignore_errors=True)
+def clean_export_zip_file():
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
     yield
     yield
-    shutil.rmtree("./tmp", ignore_errors=True)
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
 
 
 
 
 def plus_1(x):
 def plus_1(x):
@@ -57,15 +59,7 @@ def configure_test_scenario(input_data, frequency=None):
     return scenario_cfg
     return scenario_cfg
 
 
 
 
-def test_export_scenario_to_the_storage_folder(init_sql_repo):
-    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
-    scenario = tp.create_scenario(scenario_cfg)
-
-    with pytest.raises(InvalidExportPath):
-        tp.export_scenario(scenario.id, Config.core.taipy_storage_folder)
-
-
-def test_export_scenario_with_cycle(init_sql_repo):
+def test_export_scenario_with_cycle(tmp_path, init_sql_repo):
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
 
 
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
@@ -73,9 +67,11 @@ def test_export_scenario_with_cycle(init_sql_repo):
     jobs = submission.jobs
     jobs = submission.jobs
 
 
     # Export the submitted scenario
     # Export the submitted scenario
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path)
 
 
-    assert sorted(os.listdir("./tmp/exp_scenario/data_node")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/data_node")) == sorted(
         [
         [
             f"{scenario.i_1.id}.json",
             f"{scenario.i_1.id}.json",
             f"{scenario.o_1_csv.id}.json",
             f"{scenario.o_1_csv.id}.json",
@@ -84,7 +80,7 @@ def test_export_scenario_with_cycle(init_sql_repo):
             f"{scenario.o_1_json.id}.json",
             f"{scenario.o_1_json.id}.json",
         ]
         ]
     )
     )
-    assert sorted(os.listdir("./tmp/exp_scenario/task")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/task")) == sorted(
         [
         [
             f"{scenario.t_1_csv.id}.json",
             f"{scenario.t_1_csv.id}.json",
             f"{scenario.t_1_excel.id}.json",
             f"{scenario.t_1_excel.id}.json",
@@ -92,32 +88,34 @@ def test_export_scenario_with_cycle(init_sql_repo):
             f"{scenario.t_1_json.id}.json",
             f"{scenario.t_1_json.id}.json",
         ]
         ]
     )
     )
-    assert sorted(os.listdir("./tmp/exp_scenario/scenario")) == sorted([f"{scenario.id}.json"])
-    assert sorted(os.listdir("./tmp/exp_scenario/job")) == sorted(
+    assert sorted(os.listdir(f"{tmp_path}/scenario")) == sorted([f"{scenario.id}.json"])
+    assert sorted(os.listdir(f"{tmp_path}/job")) == sorted(
         [f"{jobs[0].id}.json", f"{jobs[1].id}.json", f"{jobs[2].id}.json", f"{jobs[3].id}.json"]
         [f"{jobs[0].id}.json", f"{jobs[1].id}.json", f"{jobs[2].id}.json", f"{jobs[3].id}.json"]
     )
     )
-    assert os.listdir("./tmp/exp_scenario/submission") == [f"{submission.id}.json"]
-    assert sorted(os.listdir("./tmp/exp_scenario/cycle")) == sorted([f"{scenario.cycle.id}.json"])
+    assert os.listdir(f"{tmp_path}/submission") == [f"{submission.id}.json"]
+    assert sorted(os.listdir(f"{tmp_path}/cycle")) == sorted([f"{scenario.cycle.id}.json"])
 
 
 
 
-def test_export_scenario_without_cycle(init_sql_repo):
+def test_export_scenario_without_cycle(tmp_path, init_sql_repo):
     scenario_cfg = configure_test_scenario(1)
     scenario_cfg = configure_test_scenario(1)
 
 
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export the submitted scenario
     # Export the submitted scenario
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path)
 
 
-    assert os.path.exists("./tmp/exp_scenario/data_node")
-    assert os.path.exists("./tmp/exp_scenario/task")
-    assert os.path.exists("./tmp/exp_scenario/scenario")
-    assert os.path.exists("./tmp/exp_scenario/job")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    assert not os.path.exists("./tmp/exp_scenario/cycle")  # No cycle
+    assert os.path.exists(f"{tmp_path}/data_node")
+    assert os.path.exists(f"{tmp_path}/task")
+    assert os.path.exists(f"{tmp_path}/scenario")
+    assert os.path.exists(f"{tmp_path}/job")
+    assert os.path.exists(f"{tmp_path}/submission")
+    assert not os.path.exists(f"{tmp_path}/cycle")  # No cycle
 
 
 
 
-def test_export_scenario_override_existing_files(init_sql_repo):
+def test_export_scenario_override_existing_files(tmp_path, init_sql_repo):
     scenario_1_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_1_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
     scenario_2_cfg = configure_test_scenario(2)
     scenario_2_cfg = configure_test_scenario(2)
 
 
@@ -125,45 +123,54 @@ def test_export_scenario_override_existing_files(init_sql_repo):
     tp.submit(scenario_1)
     tp.submit(scenario_1)
 
 
     # Export the submitted scenario_1
     # Export the submitted scenario_1
-    tp.export_scenario(scenario_1.id, "./tmp/exp_scenario")
-    assert os.path.exists("./tmp/exp_scenario/data_node")
-    assert os.path.exists("./tmp/exp_scenario/task")
-    assert os.path.exists("./tmp/exp_scenario/scenario")
-    assert os.path.exists("./tmp/exp_scenario/job")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    assert os.path.exists("./tmp/exp_scenario/cycle")
+    tp.export_scenario(scenario_1.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_1")
+    assert os.path.exists(f"{tmp_path}/scenario_1/data_node")
+    assert os.path.exists(f"{tmp_path}/scenario_1/task")
+    assert os.path.exists(f"{tmp_path}/scenario_1/scenario")
+    assert os.path.exists(f"{tmp_path}/scenario_1/job")
+    assert os.path.exists(f"{tmp_path}/scenario_1/submission")
+    assert os.path.exists(f"{tmp_path}/scenario_1/cycle")
 
 
     scenario_2 = tp.create_scenario(scenario_2_cfg)
     scenario_2 = tp.create_scenario(scenario_2_cfg)
     tp.submit(scenario_2)
     tp.submit(scenario_2)
 
 
     # Export the submitted scenario_2 to the same folder should raise an error
     # Export the submitted scenario_2 to the same folder should raise an error
-    with pytest.raises(ExportFolderAlreadyExists):
-        tp.export_scenario(scenario_2.id, "./tmp/exp_scenario")
+    with pytest.raises(ExportPathAlreadyExists):
+        tp.export_scenario(scenario_2.id, "tmp.zip")
 
 
     # Export the submitted scenario_2 without a cycle and override the existing files
     # Export the submitted scenario_2 without a cycle and override the existing files
-    tp.export_scenario(scenario_2.id, "./tmp/exp_scenario", override=True)
-    assert os.path.exists("./tmp/exp_scenario/data_node")
-    assert os.path.exists("./tmp/exp_scenario/task")
-    assert os.path.exists("./tmp/exp_scenario/scenario")
-    assert os.path.exists("./tmp/exp_scenario/job")
-    assert os.path.exists("./tmp/exp_scenario/submission")
-    # The cycles folder should be removed when overriding
-    assert not os.path.exists("./tmp/exp_scenario/cycle")
-
-
-def test_export_scenario_filesystem_with_data(init_sql_repo):
+    tp.export_scenario(scenario_2.id, "tmp.zip", override=True)
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_2")
+    assert os.path.exists(f"{tmp_path}/scenario_2/data_node")
+    assert os.path.exists(f"{tmp_path}/scenario_2/task")
+    assert os.path.exists(f"{tmp_path}/scenario_2/scenario")
+    assert os.path.exists(f"{tmp_path}/scenario_2/job")
+    assert os.path.exists(f"{tmp_path}/scenario_2/submission")
+    # The cycles folder should not exists since the new scenario does not have a cycle
+    assert not os.path.exists(f"{tmp_path}/scenario_2/cycle")
+
+
+def test_export_scenario_sql_repo_with_data(tmp_path, init_sql_repo):
     scenario_cfg = configure_test_scenario(1)
     scenario_cfg = configure_test_scenario(1)
     scenario = tp.create_scenario(scenario_cfg)
     scenario = tp.create_scenario(scenario_cfg)
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export scenario without data
     # Export scenario without data
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario")
-    assert not os.path.exists("./tmp/exp_scenario/user_data")
+    tp.export_scenario(scenario.id, "tmp.zip")
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_without_data")
+    assert not os.path.exists(f"{tmp_path}/scenario_without_data/user_data")
 
 
     # Export scenario with data
     # Export scenario with data
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario", include_data=True, override=True)
-    assert os.path.exists("./tmp/exp_scenario/user_data")
-    data_files = [f for _, _, files in os.walk("./tmp/exp_scenario/user_data") for f in files]
+    tp.export_scenario(scenario.id, "tmp.zip", include_data=True, override=True)
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path / "scenario_with_data")
+    assert os.path.exists(f"{tmp_path}/scenario_with_data/user_data")
+
+    data_files = [f for _, _, files in os.walk(f"{tmp_path}/scenario_with_data/user_data") for f in files]
     assert sorted(data_files) == sorted(
     assert sorted(data_files) == sorted(
         [
         [
             f"{scenario.i_1.id}.p",
             f"{scenario.i_1.id}.p",
@@ -188,6 +195,6 @@ def test_export_non_file_based_data_node_raise_warning(init_sql_repo, caplog):
     tp.submit(scenario)
     tp.submit(scenario)
 
 
     # Export scenario with in-memory data node
     # Export scenario with in-memory data node
-    tp.export_scenario(scenario.id, "./tmp/exp_scenario", include_data=True)
+    tp.export_scenario(scenario.id, "tmp.zip", include_data=True)
     expected_warning = f"Data node {scenario.o_mem.id} is not a file-based data node and the data will not be exported"
     expected_warning = f"Data node {scenario.o_mem.id} is not a file-based data node and the data will not be exported"
     assert expected_warning in caplog.text
     assert expected_warning in caplog.text

+ 213 - 0
tests/core/test_taipy/test_import.py

@@ -0,0 +1,213 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+import shutil
+import zipfile
+
+import pandas as pd
+import pytest
+
+import taipy.core.taipy as tp
+from taipy import Config, Frequency, Scope
+from taipy.core._version._version_manager import _VersionManager
+from taipy.core.cycle._cycle_manager import _CycleManager
+from taipy.core.data._data_manager import _DataManager
+from taipy.core.exceptions.exceptions import (
+    ConflictedConfigurationError,
+    EntitiesToBeImportAlredyExist,
+    ImportArchiveDoesntContainAnyScenario,
+    ImportScenarioDoesntHaveAVersion,
+)
+from taipy.core.job._job_manager import _JobManager
+from taipy.core.scenario._scenario_manager import _ScenarioManager
+from taipy.core.submission._submission_manager import _SubmissionManager
+from taipy.core.task._task_manager import _TaskManager
+
+
+@pytest.fixture(scope="function", autouse=True)
+def clean_export_zip_file():
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
+    yield
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
+
+
+def plus_1(x):
+    return x + 1
+
+
+def plus_1_dataframe(x):
+    return pd.DataFrame({"output": [x + 1]})
+
+
+def configure_test_scenario(input_data, frequency=None):
+    input_cfg = Config.configure_data_node(
+        id=f"i_{input_data}", storage_type="pickle", scope=Scope.SCENARIO, default_data=input_data
+    )
+    csv_output_cfg = Config.configure_data_node(id=f"o_{input_data}_csv", storage_type="csv")
+    excel_output_cfg = Config.configure_data_node(id=f"o_{input_data}_excel", storage_type="excel")
+    parquet_output_cfg = Config.configure_data_node(id=f"o_{input_data}_parquet", storage_type="parquet")
+    json_output_cfg = Config.configure_data_node(id=f"o_{input_data}_json", storage_type="json")
+
+    csv_task_cfg = Config.configure_task(f"t_{input_data}_csv", plus_1_dataframe, input_cfg, csv_output_cfg)
+    excel_task_cfg = Config.configure_task(f"t_{input_data}_excel", plus_1_dataframe, input_cfg, excel_output_cfg)
+    parquet_task_cfg = Config.configure_task(f"t_{input_data}_parquet", plus_1_dataframe, input_cfg, parquet_output_cfg)
+    json_task_cfg = Config.configure_task(f"t_{input_data}_json", plus_1, input_cfg, json_output_cfg)
+    scenario_cfg = Config.configure_scenario(
+        id=f"s_{input_data}",
+        task_configs=[csv_task_cfg, excel_task_cfg, parquet_task_cfg, json_task_cfg],
+        frequency=frequency,
+    )
+
+    return scenario_cfg
+
+
+def export_test_scenario(scenario_cfg, export_path="tmp.zip", override=False, include_data=False):
+    scenario = tp.create_scenario(scenario_cfg)
+    tp.submit(scenario)
+
+    # Export the submitted scenario
+    tp.export_scenario(scenario.id, export_path, override, include_data)
+    return scenario
+
+
+def test_import_scenario_without_data(init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    scenario = export_test_scenario(scenario_cfg)
+
+    init_managers()
+
+    assert _ScenarioManager._get_all() == []
+    imported_scenario = tp.import_scenario("tmp.zip")
+
+    # The imported scenario should be the same as the exported scenario
+    assert _ScenarioManager._get_all() == [imported_scenario]
+    assert imported_scenario == scenario
+
+    # All entities belonging to the scenario should be imported
+    assert len(_CycleManager._get_all()) == 1
+    assert len(_TaskManager._get_all()) == 4
+    assert len(_DataManager._get_all()) == 5
+    assert len(_JobManager._get_all()) == 4
+    assert len(_SubmissionManager._get_all()) == 1
+    assert len(_VersionManager._get_all()) == 1
+
+
+def test_import_scenario_with_data(init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg, include_data=True)
+
+    init_managers()
+
+    assert _ScenarioManager._get_all() == []
+    imported_scenario = tp.import_scenario("tmp.zip")
+
+    # All data of all data nodes should be imported
+    assert all(os.path.exists(dn.path) for dn in imported_scenario.data_nodes.values())
+
+
+def test_import_scenario_when_entities_are_already_existed_should_rollback(caplog):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    caplog.clear()
+
+    _CycleManager._delete_all()
+    _TaskManager._delete_all()
+    _DataManager._delete_all()
+    _JobManager._delete_all()
+    _ScenarioManager._delete_all()
+
+    assert len(_CycleManager._get_all()) == 0
+    assert len(_TaskManager._get_all()) == 0
+    assert len(_DataManager._get_all()) == 0
+    assert len(_JobManager._get_all()) == 0
+    assert len(_SubmissionManager._get_all()) == 1  # Keep the submission entity to test the rollback
+    submission_id = _SubmissionManager._get_all()[0].id
+    assert len(_ScenarioManager._get_all()) == 0
+
+    # Import the scenario when the old entities still exist should raise an error
+    with pytest.raises(EntitiesToBeImportAlredyExist):
+        tp.import_scenario("tmp.zip")
+    assert all(log.levelname in ["ERROR", "INFO"] for log in caplog.records)
+    assert "An error occurred during the import" in caplog.text
+    assert f"{submission_id} already exists. Please use the 'override' parameter to override it" in caplog.text
+
+    # No entity should be imported and the old entities should be kept
+    assert len(_CycleManager._get_all()) == 0
+    assert len(_TaskManager._get_all()) == 0
+    assert len(_DataManager._get_all()) == 0
+    assert len(_JobManager._get_all()) == 0
+    assert len(_SubmissionManager._get_all()) == 1  # Keep the submission entity to test the rollback
+    assert len(_ScenarioManager._get_all()) == 0
+
+    caplog.clear()
+
+    # Import with override flag
+    tp.import_scenario("tmp.zip", override=True)
+    assert all(log.levelname in ["WARNING", "INFO"] for log in caplog.records)
+    assert f"{submission_id} already exists and will be overridden" in caplog.text
+
+    # The scenario is imported and overridden the old one
+    assert len(_ScenarioManager._get_all()) == 1
+    assert len(_CycleManager._get_all()) == 1
+    assert len(_TaskManager._get_all()) == 4
+    assert len(_DataManager._get_all()) == 5
+    assert len(_JobManager._get_all()) == 4
+    assert len(_SubmissionManager._get_all()) == 1
+    assert len(_VersionManager._get_all()) == 1
+
+
+def test_import_incompatible_scenario(init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    Config.unblock_update()
+
+    # Configure a new dn to make the exported version incompatible
+    Config.configure_data_node("new_dn")
+
+    with pytest.raises(ConflictedConfigurationError):
+        tp.import_scenario("tmp.zip")
+
+
+def test_import_a_non_exists_folder():
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    with pytest.raises(FileNotFoundError):
+        tp.import_scenario("non_exists_folder")
+
+
+def test_import_an_empty_archive(tmpdir_factory):
+    empty_folder = tmpdir_factory.mktemp("empty_folder").strpath
+    shutil.make_archive("tmp", "zip", empty_folder)
+
+    with pytest.raises(ImportArchiveDoesntContainAnyScenario):
+        tp.import_scenario("tmp.zip")
+
+
+def test_import_with_no_version(tmp_path):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    # Extract the zip,
+    with zipfile.ZipFile("./tmp.zip") as zip_file:
+        zip_file.extractall(tmp_path)
+    # remove the version,
+    shutil.rmtree(f"{tmp_path}/version")
+    # and archive the scenario without the version again
+    shutil.make_archive("tmp", "zip", tmp_path)
+
+    with pytest.raises(ImportScenarioDoesntHaveAVersion):
+        tp.import_scenario("tmp.zip")

+ 174 - 0
tests/core/test_taipy/test_import_with_sql_repo.py

@@ -0,0 +1,174 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+import os
+
+import pandas as pd
+import pytest
+
+import taipy.core.taipy as tp
+from taipy import Config, Frequency, Scope
+from taipy.core._version._version_manager import _VersionManager
+from taipy.core.cycle._cycle_manager import _CycleManager
+from taipy.core.data._data_manager import _DataManager
+from taipy.core.exceptions.exceptions import ConflictedConfigurationError, EntitiesToBeImportAlredyExist
+from taipy.core.job._job_manager import _JobManager
+from taipy.core.scenario._scenario_manager import _ScenarioManager
+from taipy.core.submission._submission_manager import _SubmissionManager
+from taipy.core.task._task_manager import _TaskManager
+
+
+@pytest.fixture(scope="function", autouse=True)
+def clean_export_zip_file():
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
+    yield
+    if os.path.exists("./tmp.zip"):
+        os.remove("./tmp.zip")
+
+
+def plus_1(x):
+    return x + 1
+
+
+def plus_1_dataframe(x):
+    return pd.DataFrame({"output": [x + 1]})
+
+
+def configure_test_scenario(input_data, frequency=None):
+    input_cfg = Config.configure_data_node(
+        id=f"i_{input_data}", storage_type="pickle", scope=Scope.SCENARIO, default_data=input_data
+    )
+    csv_output_cfg = Config.configure_data_node(id=f"o_{input_data}_csv", storage_type="csv")
+    excel_output_cfg = Config.configure_data_node(id=f"o_{input_data}_excel", storage_type="excel")
+    parquet_output_cfg = Config.configure_data_node(id=f"o_{input_data}_parquet", storage_type="parquet")
+    json_output_cfg = Config.configure_data_node(id=f"o_{input_data}_json", storage_type="json")
+
+    csv_task_cfg = Config.configure_task(f"t_{input_data}_csv", plus_1_dataframe, input_cfg, csv_output_cfg)
+    excel_task_cfg = Config.configure_task(f"t_{input_data}_excel", plus_1_dataframe, input_cfg, excel_output_cfg)
+    parquet_task_cfg = Config.configure_task(f"t_{input_data}_parquet", plus_1_dataframe, input_cfg, parquet_output_cfg)
+    json_task_cfg = Config.configure_task(f"t_{input_data}_json", plus_1, input_cfg, json_output_cfg)
+    scenario_cfg = Config.configure_scenario(
+        id=f"s_{input_data}",
+        task_configs=[csv_task_cfg, excel_task_cfg, parquet_task_cfg, json_task_cfg],
+        frequency=frequency,
+    )
+
+    return scenario_cfg
+
+
+def export_test_scenario(scenario_cfg, export_path="tmp.zip", override=False, include_data=False):
+    scenario = tp.create_scenario(scenario_cfg)
+    tp.submit(scenario)
+
+    # Export the submitted scenario
+    tp.export_scenario(scenario.id, export_path, override, include_data)
+    return scenario
+
+
+def test_import_scenario_without_data(init_sql_repo, init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    scenario = export_test_scenario(scenario_cfg)
+
+    init_managers()
+
+    assert _ScenarioManager._get_all() == []
+    imported_scenario = tp.import_scenario("tmp.zip")
+
+    # The imported scenario should be the same as the exported scenario
+    assert _ScenarioManager._get_all() == [imported_scenario]
+    assert imported_scenario == scenario
+
+    # All entities belonging to the scenario should be imported
+    assert len(_CycleManager._get_all()) == 1
+    assert len(_TaskManager._get_all()) == 4
+    assert len(_DataManager._get_all()) == 5
+    assert len(_JobManager._get_all()) == 4
+    assert len(_SubmissionManager._get_all()) == 1
+    assert len(_VersionManager._get_all()) == 1
+
+
+def test_import_scenario_with_data(init_sql_repo, init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg, include_data=True)
+
+    init_managers()
+
+    assert _ScenarioManager._get_all() == []
+    imported_scenario = tp.import_scenario("tmp.zip")
+
+    # All data of all data nodes should be imported
+    assert all(os.path.exists(dn.path) for dn in imported_scenario.data_nodes.values())
+
+
+def test_import_scenario_when_entities_are_already_existed_should_rollback(init_sql_repo, caplog):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    caplog.clear()
+
+    _CycleManager._delete_all()
+    _TaskManager._delete_all()
+    _DataManager._delete_all()
+    _JobManager._delete_all()
+    _ScenarioManager._delete_all()
+
+    assert len(_CycleManager._get_all()) == 0
+    assert len(_TaskManager._get_all()) == 0
+    assert len(_DataManager._get_all()) == 0
+    assert len(_JobManager._get_all()) == 0
+    assert len(_SubmissionManager._get_all()) == 1  # Keep the submission entity to test the rollback
+    submission_id = _SubmissionManager._get_all()[0].id
+    assert len(_ScenarioManager._get_all()) == 0
+
+    # Import the scenario when the old entities still exist should raise an error
+    with pytest.raises(EntitiesToBeImportAlredyExist):
+        tp.import_scenario("tmp.zip")
+    assert all(log.levelname in ["ERROR", "INFO"] for log in caplog.records)
+    assert "An error occurred during the import" in caplog.text
+    assert f"{submission_id} already exists. Please use the 'override' parameter to override it" in caplog.text
+
+    # No entity should be imported and the old entities should be kept
+    assert len(_CycleManager._get_all()) == 0
+    assert len(_TaskManager._get_all()) == 0
+    assert len(_DataManager._get_all()) == 0
+    assert len(_JobManager._get_all()) == 0
+    assert len(_SubmissionManager._get_all()) == 1  # Keep the submission entity to test the rollback
+    assert len(_ScenarioManager._get_all()) == 0
+
+    caplog.clear()
+
+    # Import with override flag
+    tp.import_scenario("tmp.zip", override=True)
+    assert all(log.levelname in ["WARNING", "INFO"] for log in caplog.records)
+    assert f"{submission_id} already exists and will be overridden" in caplog.text
+
+    # The scenario is imported and overridden the old one
+    assert len(_ScenarioManager._get_all()) == 1
+    assert len(_CycleManager._get_all()) == 1
+    assert len(_TaskManager._get_all()) == 4
+    assert len(_DataManager._get_all()) == 5
+    assert len(_JobManager._get_all()) == 4
+    assert len(_SubmissionManager._get_all()) == 1
+    assert len(_VersionManager._get_all()) == 1
+
+
+def test_import_incompatible_scenario(init_sql_repo, init_managers):
+    scenario_cfg = configure_test_scenario(1, frequency=Frequency.DAILY)
+    export_test_scenario(scenario_cfg)
+
+    Config.unblock_update()
+
+    # Configure a new dn to make the exported version incompatible
+    Config.configure_data_node("new_dn")
+
+    with pytest.raises(ConflictedConfigurationError):
+        tp.import_scenario("tmp.zip")

+ 4 - 4
tests/gui/utils/test_types.py

@@ -15,16 +15,16 @@ import warnings
 import pytest
 import pytest
 
 
 from taipy.gui.utils.date import _string_to_date
 from taipy.gui.utils.date import _string_to_date
-from taipy.gui.utils.types import _TaipyBase, _TaipyBool, _TaipyDate, _TaipyNumber
+from taipy.gui.utils.types import _TaipyBool, _TaipyData, _TaipyDate, _TaipyNumber
 
 
 
 
-def test_taipy_base():
-    tb = _TaipyBase("value", "hash")
+def test_taipy_data():
+    tb = _TaipyData("value", "hash")
     assert tb.get() == "value"
     assert tb.get() == "value"
     assert tb.get_name() == "hash"
     assert tb.get_name() == "hash"
     tb.set("a value")
     tb.set("a value")
     assert tb.get() == "a value"
     assert tb.get() == "a value"
-    assert tb.get_hash() == NotImplementedError
+    assert tb.get_hash() == "_TpD"
 
 
 
 
 def test_taipy_bool():
 def test_taipy_bool():

+ 12 - 8
tests/gui_core/test_context_is_deletable.py

@@ -63,11 +63,12 @@ class TestGuiCoreContext_is_deletable:
                         True,
                         True,
                         True,
                         True,
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_sc_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).startswith("Error deleting Scenario.")
             assert str(assign.call_args.args[1]).startswith("Error deleting Scenario.")
 
 
             with patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_deletable_false):
             with patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_deletable_false):
@@ -82,11 +83,12 @@ class TestGuiCoreContext_is_deletable:
                             True,
                             True,
                             True,
                             True,
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sc_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not deletable.")
                 assert str(assign.call_args.args[1]).endswith("is not deletable.")
 
 
     def test_act_on_jobs(self):
     def test_act_on_jobs(self):
@@ -101,11 +103,12 @@ class TestGuiCoreContext_is_deletable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": [a_job.id], "action": "delete"},
                         {"id": [a_job.id], "action": "delete"},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_js_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).find("is not deletable.") == -1
             assert str(assign.call_args.args[1]).find("is not deletable.") == -1
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -116,9 +119,10 @@ class TestGuiCoreContext_is_deletable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": [a_job.id], "action": "delete"},
                             {"id": [a_job.id], "action": "delete"},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_js_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")

+ 39 - 25
tests/gui_core/test_context_is_editable.py

@@ -62,7 +62,8 @@ class TestGuiCoreContext_is_editable:
                         True,
                         True,
                         False,
                         False,
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_not_called()
             assign.assert_not_called()
@@ -79,11 +80,12 @@ class TestGuiCoreContext_is_editable:
                             True,
                             True,
                             False,
                             False,
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sc_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
 
 
     def test_edit_entity(self):
     def test_edit_entity(self):
@@ -96,11 +98,12 @@ class TestGuiCoreContext_is_editable:
                 {
                 {
                     "args": [
                     "args": [
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_sv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
@@ -111,11 +114,12 @@ class TestGuiCoreContext_is_editable:
                     {
                     {
                         "args": [
                         "args": [
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
 
 
     def test_act_on_jobs(self):
     def test_act_on_jobs(self):
@@ -130,11 +134,12 @@ class TestGuiCoreContext_is_editable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": [a_job.id], "action": "cancel"},
                         {"id": [a_job.id], "action": "cancel"},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_js_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).find("is not editable.") == -1
             assert str(assign.call_args.args[1]).find("is not editable.") == -1
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -145,11 +150,12 @@ class TestGuiCoreContext_is_editable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": [a_job.id], "action": "cancel"},
                             {"id": [a_job.id], "action": "cancel"},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_js_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_edit_data_node(self):
     def test_edit_data_node(self):
@@ -162,11 +168,12 @@ class TestGuiCoreContext_is_editable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_dv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
@@ -177,11 +184,12 @@ class TestGuiCoreContext_is_editable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
 
 
     def test_lock_datanode_for_edit(self):
     def test_lock_datanode_for_edit(self):
@@ -196,11 +204,12 @@ class TestGuiCoreContext_is_editable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_dv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
             with patch("taipy.gui_core._context.is_editable", side_effect=mock_is_editable_false):
@@ -211,11 +220,12 @@ class TestGuiCoreContext_is_editable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
 
 
     def test_update_data(self):
     def test_update_data(self):
@@ -230,11 +240,12 @@ class TestGuiCoreContext_is_editable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called()
             assign.assert_called()
-            assert assign.call_args_list[0].args[0] == "gui_core_dv_error"
+            assert assign.call_args_list[0].args[0] == "error_var"
             assert assign.call_args_list[0].args[1] == ""
             assert assign.call_args_list[0].args[1] == ""
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -245,11 +256,12 @@ class TestGuiCoreContext_is_editable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
 
 
     def test_tabular_data_edit(self):
     def test_tabular_data_edit(self):
@@ -263,10 +275,11 @@ class TestGuiCoreContext_is_editable:
                 "",
                 "",
                 {
                 {
                     "user_data": {"dn_id": a_datanode.id},
                     "user_data": {"dn_id": a_datanode.id},
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args_list[0].args[0] == "gui_core_dv_error"
+            assert assign.call_args_list[0].args[0] == "error_var"
             assert (
             assert (
                 assign.call_args_list[0].args[1]
                 assign.call_args_list[0].args[1]
                 == "Error updating Datanode tabular value: type does not support at[] indexer."
                 == "Error updating Datanode tabular value: type does not support at[] indexer."
@@ -279,8 +292,9 @@ class TestGuiCoreContext_is_editable:
                     "",
                     "",
                     {
                     {
                         "user_data": {"dn_id": a_datanode.id},
                         "user_data": {"dn_id": a_datanode.id},
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not editable.")
                 assert str(assign.call_args.args[1]).endswith("is not editable.")

+ 6 - 4
tests/gui_core/test_context_is_promotable.py

@@ -59,11 +59,12 @@ class TestGuiCoreContext_is_promotable:
                 {
                 {
                     "args": [
                     "args": [
                         {"name": "name", "id": a_scenario.id, "primary": True},
                         {"name": "name", "id": a_scenario.id, "primary": True},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_sv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).endswith("to primary because it doesn't belong to a cycle.")
             assert str(assign.call_args.args[1]).endswith("to primary because it doesn't belong to a cycle.")
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -74,9 +75,10 @@ class TestGuiCoreContext_is_promotable:
                     {
                     {
                         "args": [
                         "args": [
                             {"name": "name", "id": a_scenario.id, "primary": True},
                             {"name": "name", "id": a_scenario.id, "primary": True},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not promotable.")
                 assert str(assign.call_args.args[1]).endswith("is not promotable.")

+ 66 - 32
tests/gui_core/test_context_is_readable.py

@@ -11,15 +11,18 @@
 
 
 import contextlib
 import contextlib
 import typing as t
 import typing as t
+from datetime import datetime
 from unittest.mock import Mock, patch
 from unittest.mock import Mock, patch
 
 
+from taipy.config.common.frequency import Frequency
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
-from taipy.core import Job, JobId, Scenario, Task
+from taipy.core import Cycle, CycleId, Job, JobId, Scenario, Task
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.submission.submission import Submission, SubmissionStatus
 from taipy.core.submission.submission import Submission, SubmissionStatus
 from taipy.gui import Gui
 from taipy.gui import Gui
 from taipy.gui_core._context import _GuiCoreContext
 from taipy.gui_core._context import _GuiCoreContext
 
 
+a_cycle = Cycle(Frequency.DAILY, {}, datetime.now(), datetime.now(), datetime.now(), id=CycleId("CYCLE_id"))
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 a_task = Task("task_config_id", {}, print)
 a_task = Task("task_config_id", {}, print)
 a_job = Job(t.cast(JobId, "JOB_job_id"), a_task, "submit_id", a_scenario.id)
 a_job = Job(t.cast(JobId, "JOB_job_id"), a_task, "submit_id", a_scenario.id)
@@ -50,6 +53,8 @@ def mock_core_get(entity_id):
         return a_datanode
         return a_datanode
     if entity_id == a_submission.id:
     if entity_id == a_submission.id:
         return a_submission
         return a_submission
+    if entity_id == a_cycle.id:
+        return a_cycle
     return a_task
     return a_task
 
 
 
 
@@ -62,14 +67,27 @@ class TestGuiCoreContext_is_readable:
     def test_scenario_adapter(self):
     def test_scenario_adapter(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
             gui_core_context = _GuiCoreContext(Mock())
             gui_core_context = _GuiCoreContext(Mock())
+            gui_core_context.scenario_by_cycle = {}
             outcome = gui_core_context.scenario_adapter(a_scenario)
             outcome = gui_core_context.scenario_adapter(a_scenario)
-            assert isinstance(outcome, tuple)
+            assert isinstance(outcome, list)
             assert outcome[0] == a_scenario.id
             assert outcome[0] == a_scenario.id
 
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
                 outcome = gui_core_context.scenario_adapter(a_scenario)
                 outcome = gui_core_context.scenario_adapter(a_scenario)
                 assert outcome is None
                 assert outcome is None
 
 
+    def test_cycle_adapter(self):
+        with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
+            gui_core_context = _GuiCoreContext(Mock())
+            gui_core_context.scenario_by_cycle = {"a": 1}
+            outcome = gui_core_context.cycle_adapter(a_cycle)
+            assert isinstance(outcome, list)
+            assert outcome[0] == a_cycle.id
+
+            with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
+                outcome = gui_core_context.cycle_adapter(a_cycle)
+                assert outcome is None
+
     def test_get_scenario_by_id(self):
     def test_get_scenario_by_id(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
             gui_core_context = _GuiCoreContext(Mock())
             gui_core_context = _GuiCoreContext(Mock())
@@ -94,7 +112,8 @@ class TestGuiCoreContext_is_readable:
                         True,
                         True,
                         False,
                         False,
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_not_called()
             assign.assert_not_called()
@@ -111,11 +130,12 @@ class TestGuiCoreContext_is_readable:
                             True,
                             True,
                             False,
                             False,
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sc_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_edit_entity(self):
     def test_edit_entity(self):
@@ -128,11 +148,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_sv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
@@ -143,11 +164,12 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_submission_status_callback(self):
     def test_submission_status_callback(self):
@@ -209,11 +231,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": [a_job.id], "action": "delete"},
                         {"id": [a_job.id], "action": "delete"},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_js_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).find("is not readable.") == -1
             assert str(assign.call_args.args[1]).find("is not readable.") == -1
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -223,11 +246,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": [a_job.id], "action": "cancel"},
                         {"id": [a_job.id], "action": "cancel"},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_js_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).find("is not readable.") == -1
             assert str(assign.call_args.args[1]).find("is not readable.") == -1
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -238,11 +262,12 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": [a_job.id], "action": "delete"},
                             {"id": [a_job.id], "action": "delete"},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_js_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assign.reset_mock()
                 assign.reset_mock()
 
 
@@ -252,11 +277,12 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": [a_job.id], "action": "cancel"},
                             {"id": [a_job.id], "action": "cancel"},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_js_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_edit_data_node(self):
     def test_edit_data_node(self):
@@ -269,11 +295,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_dv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
@@ -284,11 +311,12 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_lock_datanode_for_edit(self):
     def test_lock_datanode_for_edit(self):
@@ -303,11 +331,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_dv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[1] == ""
             assert assign.call_args.args[1] == ""
 
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
@@ -318,17 +347,18 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_get_scenarios_for_owner(self):
     def test_get_scenarios_for_owner(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
             gui_core_context = _GuiCoreContext(Mock())
             gui_core_context = _GuiCoreContext(Mock())
-            gui_core_context.get_scenarios_for_owner(a_scenario.id, '')
+            gui_core_context.get_scenarios_for_owner(a_scenario.id, "")
             mockget.assert_called_once()
             mockget.assert_called_once()
             mockget.reset_mock()
             mockget.reset_mock()
 
 
@@ -348,11 +378,12 @@ class TestGuiCoreContext_is_readable:
                 {
                 {
                     "args": [
                     "args": [
                         {"id": a_datanode.id},
                         {"id": a_datanode.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called()
             assign.assert_called()
-            assert assign.call_args_list[0].args[0] == "gui_core_dv_error"
+            assert assign.call_args_list[0].args[0] == "error_var"
             assert assign.call_args_list[0].args[1] == ""
             assert assign.call_args_list[0].args[1] == ""
             assign.reset_mock()
             assign.reset_mock()
 
 
@@ -363,11 +394,12 @@ class TestGuiCoreContext_is_readable:
                     {
                     {
                         "args": [
                         "args": [
                             {"id": a_datanode.id},
                             {"id": a_datanode.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_tabular_data_edit(self):
     def test_tabular_data_edit(self):
@@ -381,10 +413,11 @@ class TestGuiCoreContext_is_readable:
                 "",
                 "",
                 {
                 {
                     "user_data": {"dn_id": a_datanode.id},
                     "user_data": {"dn_id": a_datanode.id},
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args_list[0].args[0] == "gui_core_dv_error"
+            assert assign.call_args_list[0].args[0] == "error_var"
             assert (
             assert (
                 assign.call_args_list[0].args[1]
                 assign.call_args_list[0].args[1]
                 == "Error updating Datanode tabular value: type does not support at[] indexer."
                 == "Error updating Datanode tabular value: type does not support at[] indexer."
@@ -397,10 +430,11 @@ class TestGuiCoreContext_is_readable:
                     "",
                     "",
                     {
                     {
                         "user_data": {"dn_id": a_datanode.id},
                         "user_data": {"dn_id": a_datanode.id},
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_dv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
 
     def test_get_data_node_tabular_data(self):
     def test_get_data_node_tabular_data(self):

+ 6 - 4
tests/gui_core/test_context_is_submitable.py

@@ -59,11 +59,12 @@ class TestGuiCoreContext_is_submittable:
                 {
                 {
                     "args": [
                     "args": [
                         {"name": "name", "id": a_scenario.id},
                         {"name": "name", "id": a_scenario.id},
-                    ]
+                    ],
+                    "error_id": "error_var",
                 },
                 },
             )
             )
             assign.assert_called_once()
             assign.assert_called_once()
-            assert assign.call_args.args[0] == "gui_core_sv_error"
+            assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).startswith("Error submitting entity.")
             assert str(assign.call_args.args[1]).startswith("Error submitting entity.")
 
 
             with patch("taipy.gui_core._context.is_submittable", side_effect=mock_is_submittable_false):
             with patch("taipy.gui_core._context.is_submittable", side_effect=mock_is_submittable_false):
@@ -74,9 +75,10 @@ class TestGuiCoreContext_is_submittable:
                     {
                     {
                         "args": [
                         "args": [
                             {"name": "name", "id": a_scenario.id},
                             {"name": "name", "id": a_scenario.id},
-                        ]
+                        ],
+                        "error_id": "error_var",
                     },
                     },
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
-                assert assign.call_args.args[0] == "gui_core_sv_error"
+                assert assign.call_args.args[0] == "error_var"
                 assert str(assign.call_args.args[1]).endswith("is not submittable.")
                 assert str(assign.call_args.args[1]).endswith("is not submittable.")

+ 16 - 0
tests/rest/conftest.py

@@ -13,6 +13,7 @@ import os
 import shutil
 import shutil
 import uuid
 import uuid
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
+from queue import Queue
 
 
 import pandas as pd
 import pandas as pd
 import pytest
 import pytest
@@ -22,6 +23,7 @@ from taipy.config import Config
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 from taipy.core import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
 from taipy.core import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
+from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.job._job_manager import _JobManager
 from taipy.core.job._job_manager import _JobManager
@@ -323,3 +325,17 @@ def cleanup_files(reset_configuration_singleton, inject_core_sections):
     for path in [".data", ".my_data", "user_data", ".taipy"]:
     for path in [".data", ".my_data", "user_data", ".taipy"]:
         if os.path.exists(path):
         if os.path.exists(path):
             shutil.rmtree(path, ignore_errors=True)
             shutil.rmtree(path, ignore_errors=True)
+
+
+@pytest.fixture
+def init_orchestrator():
+    def _init_orchestrator():
+        _OrchestratorFactory._remove_dispatcher()
+
+        if _OrchestratorFactory._orchestrator is None:
+            _OrchestratorFactory._build_orchestrator()
+        _OrchestratorFactory._build_dispatcher(force_restart=True)
+        _OrchestratorFactory._orchestrator.jobs_to_run = Queue()
+        _OrchestratorFactory._orchestrator.blocked_jobs = []
+
+    return _init_orchestrator

+ 3 - 1
tests/rest/test_end_to_end.py

@@ -60,7 +60,7 @@ def delete(url, client):
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
-def test_end_to_end(client, setup_end_to_end):
+def test_end_to_end(client, setup_end_to_end, init_orchestrator):
     # Create Scenario: Should also create all of its dependencies(sequences, tasks, datanodes, etc)
     # Create Scenario: Should also create all of its dependencies(sequences, tasks, datanodes, etc)
     scenario = create_and_submit_scenario("scenario", client)
     scenario = create_and_submit_scenario("scenario", client)
 
 
@@ -104,3 +104,5 @@ def test_end_to_end(client, setup_end_to_end):
     url_without_slash = url_for("api.scenarios")[:-1]
     url_without_slash = url_for("api.scenarios")[:-1]
     get_all(url_with_slash, 1, client)
     get_all(url_with_slash, 1, client)
     get_all(url_without_slash, 1, client)
     get_all(url_without_slash, 1, client)
+
+    init_orchestrator()

+ 7 - 8
tests/rest/test_scenario.py

@@ -11,7 +11,6 @@
 
 
 from unittest import mock
 from unittest import mock
 
 
-import pytest
 from flask import url_for
 from flask import url_for
 
 
 
 
@@ -75,16 +74,16 @@ def test_get_all_scenarios(client, default_sequence, default_scenario_config_lis
     assert len(results) == 10
     assert len(results) == 10
 
 
 
 
-@pytest.mark.xfail()
-def test_execute_scenario(client, default_scenario):
+def test_execute_scenario(client, default_scenario_config):
     # test 404
     # test 404
     user_url = url_for("api.scenario_submit", scenario_id="foo")
     user_url = url_for("api.scenario_submit", scenario_id="foo")
     rep = client.post(user_url)
     rep = client.post(user_url)
     assert rep.status_code == 404
     assert rep.status_code == 404
 
 
-    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get") as manager_mock:
-        manager_mock.return_value = default_scenario
+    with mock.patch("taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock:
+        config_mock.return_value = default_scenario_config
+        scenarios_url = url_for("api.scenarios", config_id="bar")
+        scn = client.post(scenarios_url)
 
 
-        # test get_scenario
-        rep = client.post(url_for("api.scenario_submit", scenario_id="foo"))
-        assert rep.status_code == 200
+    rep = client.post(url_for("api.scenario_submit", scenario_id=scn.json["scenario"]["id"]))
+    assert rep.status_code == 200

+ 11 - 8
tests/rest/test_sequence.py

@@ -11,7 +11,6 @@
 
 
 from unittest import mock
 from unittest import mock
 
 
-import pytest
 from flask import url_for
 from flask import url_for
 
 
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
@@ -85,16 +84,20 @@ def test_get_all_sequences(client, default_scenario_config_list):
     assert len(results) == 10
     assert len(results) == 10
 
 
 
 
-@pytest.mark.xfail()
-def test_execute_sequence(client, default_sequence):
+def test_execute_sequence(client, default_scenario):
     # test 404
     # test 404
     user_url = url_for("api.sequence_submit", sequence_id="foo")
     user_url = url_for("api.sequence_submit", sequence_id="foo")
     rep = client.post(user_url)
     rep = client.post(user_url)
     assert rep.status_code == 404
     assert rep.status_code == 404
 
 
-    with mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._get") as manager_mock:
-        manager_mock.return_value = default_sequence
+    _ScenarioManagerFactory._build_manager()._set(default_scenario)
+    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get") as config_mock:
+        config_mock.return_value = default_scenario
+        sequences_url = url_for("api.sequences")
+        seq = client.post(
+            sequences_url, json={"scenario_id": default_scenario.id, "sequence_name": "sequence", "tasks": []}
+        )
 
 
-        # test get_sequence
-        rep = client.post(url_for("api.sequence_submit", sequence_id="foo"))
-        assert rep.status_code == 200
+    # test submit
+    rep = client.post(url_for("api.sequence_submit", sequence_id=seq.json["sequence"]["id"]))
+    assert rep.status_code == 200