1
0
Эх сурвалжийг харах

allows to have 2 instances of the same core viz element in a page (#1256)

* allows to have 2 instanbce of the same core viz element in a page and different configuration/error message ...
WiP scenario_selector.filter_by

* with filter

* comments from Long
fix for datanode_adapter

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 1 жил өмнө
parent
commit
a705ad954a

+ 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 ? (
                                 <>
                                 <>

+ 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):

+ 10 - 3
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

+ 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
 
 

+ 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:uniquejb>}}"
+                ),
+                "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."
                     }
                     }
                 ]
                 ]
             }
             }

+ 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.")