Forráskód Böngészése

Merge remote-tracking branch 'origin/develop' into docs/list-of-value-docs

namnguyen 6 hónapja
szülő
commit
e0d98783c9

+ 1 - 1
Pipfile

@@ -12,7 +12,7 @@ flask = "==3.0.0"
 flask-cors = "==5.0.0"
 flask-cors = "==5.0.0"
 flask-socketio = "==5.3.6"
 flask-socketio = "==5.3.6"
 Flask-RESTful = ">=0.3.9"
 Flask-RESTful = ">=0.3.9"
-gevent = "==23.7.0"
+gevent = "==24.11.1"
 gevent-websocket = "==0.10.1"
 gevent-websocket = "==0.10.1"
 gitignore-parser = "==0.1.1"
 gitignore-parser = "==0.1.1"
 kthread = "==0.2.3"
 kthread = "==0.2.3"

+ 11 - 11
frontend/taipy-gui/src/components/Taipy/Toggle.spec.tsx

@@ -124,34 +124,34 @@ describe("Toggle Component", () => {
             type: "SEND_UPDATE_ACTION",
             type: "SEND_UPDATE_ACTION",
         });
         });
     });
     });
-    it("dispatch unselected_value on deselection when allowUnselect", async () => {
+    it("dispatch nothing on deselection by default", async () => {
         const dispatch = jest.fn();
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
         const state: TaipyState = INITIAL_STATE;
         const { getByText } = render(
         const { getByText } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <Toggle lov={lov} updateVarName="varname" unselectedValue="uv" value="id2" allowUnselect={true} />
+                <Toggle lov={lov} updateVarName="varname" value="id2" />
             </TaipyContext.Provider>
             </TaipyContext.Provider>
         );
         );
         const elt = getByText("Item 2");
         const elt = getByText("Item 2");
         await userEvent.click(elt);
         await userEvent.click(elt);
-        expect(dispatch).toHaveBeenCalledWith({
-            name: "varname",
-            payload: { value: "uv" },
-            propagate: true,
-            type: "SEND_UPDATE_ACTION",
-        });
+        expect(dispatch).not.toHaveBeenCalled();
     });
     });
-    it("dispatch nothing on deselection by default", async () => {
+    it("dispatch null on deselection when allowUnselect", async () => {
         const dispatch = jest.fn();
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
         const state: TaipyState = INITIAL_STATE;
         const { getByText } = render(
         const { getByText } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <Toggle lov={lov} updateVarName="varname" unselectedValue="uv" value="id2" />
+                <Toggle lov={lov} updateVarName="varname" value="id2" allowUnselect={true} />
             </TaipyContext.Provider>
             </TaipyContext.Provider>
         );
         );
         const elt = getByText("Item 2");
         const elt = getByText("Item 2");
         await userEvent.click(elt);
         await userEvent.click(elt);
-        expect(dispatch).not.toHaveBeenCalled();
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "varname",
+            payload: { value: null },
+            propagate: true,
+            type: "SEND_UPDATE_ACTION",
+        });
     });
     });
 
 
     describe("As Switch", () => {
     describe("As Switch", () => {

+ 8 - 20
frontend/taipy-gui/src/components/Taipy/Toggle.tsx

@@ -14,26 +14,25 @@
 import React, { MouseEvent, SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react";
 import React, { MouseEvent, SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import Switch from "@mui/material/Switch";
 import Switch from "@mui/material/Switch";
-import Typography from "@mui/material/Typography";
 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 Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
 
 
+import { FormControlLabel, SxProps } from "@mui/material";
 import { createSendUpdateAction } from "../../context/taipyReducers";
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import ThemeToggle, { emptyStyle } from "./ThemeToggle";
-import { LovProps, useLovListMemo } from "./lovUtils";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
-import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 import { Icon, IconAvatar } from "../../utils/icon";
 import { Icon, IconAvatar } from "../../utils/icon";
-import { FormControlLabel, SxProps } from "@mui/material";
 import { getComponentClassName } from "./TaipyStyle";
 import { getComponentClassName } from "./TaipyStyle";
+import ThemeToggle, { emptyStyle } from "./ThemeToggle";
+import { LovProps, useLovListMemo } from "./lovUtils";
+import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 
 
 const baseGroupSx = { verticalAlign: "middle" };
 const baseGroupSx = { verticalAlign: "middle" };
 
 
 interface ToggleProps extends LovProps<string> {
 interface ToggleProps extends LovProps<string> {
     style?: SxProps;
     style?: SxProps;
     label?: string;
     label?: string;
-    unselectedValue?: string;
     allowUnselect?: boolean;
     allowUnselect?: boolean;
     mode?: string;
     mode?: string;
     isSwitch?: boolean;
     isSwitch?: boolean;
@@ -49,14 +48,13 @@ const Toggle = (props: ToggleProps) => {
         propagate = true,
         propagate = true,
         lov,
         lov,
         defaultLov = "",
         defaultLov = "",
-        unselectedValue = "",
         updateVars = "",
         updateVars = "",
         valueById,
         valueById,
         mode = "",
         mode = "",
         isSwitch = false,
         isSwitch = false,
     } = props;
     } = props;
     const dispatch = useDispatch();
     const dispatch = useDispatch();
-    const [value, setValue] = useState(props.defaultValue);
+    const [value, setValue] = useState<string | null | undefined>(props.defaultValue);
     const [bVal, setBVal] = useState(() =>
     const [bVal, setBVal] = useState(() =>
         typeof props.defaultValue === "boolean"
         typeof props.defaultValue === "boolean"
             ? props.defaultValue
             ? props.defaultValue
@@ -85,7 +83,7 @@ const Toggle = (props: ToggleProps) => {
             dispatch(
             dispatch(
                 createSendUpdateAction(
                 createSendUpdateAction(
                     updateVarName,
                     updateVarName,
-                    val === null ? unselectedValue : val,
+                    val,
                     module,
                     module,
                     props.onChange,
                     props.onChange,
                     propagate,
                     propagate,
@@ -93,17 +91,7 @@ const Toggle = (props: ToggleProps) => {
                 )
                 )
             );
             );
         },
         },
-        [
-            unselectedValue,
-            updateVarName,
-            propagate,
-            dispatch,
-            updateVars,
-            valueById,
-            props.onChange,
-            props.allowUnselect,
-            module,
-        ]
+        [updateVarName, propagate, dispatch, updateVars, valueById, props.onChange, props.allowUnselect, module]
     );
     );
 
 
     const changeSwitchValue = useCallback(
     const changeSwitchValue = useCallback(

+ 7 - 6
taipy/core/_entity/_properties.py

@@ -29,8 +29,6 @@ class _Properties(UserDict):
         super(_Properties, self).__setitem__(key, value)
         super(_Properties, self).__setitem__(key, value)
 
 
         if hasattr(self, "_entity_owner"):
         if hasattr(self, "_entity_owner"):
-            from ... import core as tp
-
             event = _make_event(
             event = _make_event(
                 self._entity_owner,
                 self._entity_owner,
                 EventOperation.UPDATE,
                 EventOperation.UPDATE,
@@ -38,7 +36,7 @@ class _Properties(UserDict):
                 attribute_value=value,
                 attribute_value=value,
             )
             )
             if not self._entity_owner._is_in_context:
             if not self._entity_owner._is_in_context:
-                tp.set(self._entity_owner)
+                self._set_entity_owner(self._entity_owner)
                 Notifier.publish(event)
                 Notifier.publish(event)
             else:
             else:
                 if key in self._pending_deletions:
                 if key in self._pending_deletions:
@@ -53,8 +51,6 @@ class _Properties(UserDict):
         super(_Properties, self).__delitem__(key)
         super(_Properties, self).__delitem__(key)
 
 
         if hasattr(self, "_entity_owner"):
         if hasattr(self, "_entity_owner"):
-            from ... import core as tp
-
             event = _make_event(
             event = _make_event(
                 self._entity_owner,
                 self._entity_owner,
                 EventOperation.UPDATE,
                 EventOperation.UPDATE,
@@ -62,9 +58,14 @@ class _Properties(UserDict):
                 attribute_value=None,
                 attribute_value=None,
             )
             )
             if not self._entity_owner._is_in_context:
             if not self._entity_owner._is_in_context:
-                tp.set(self._entity_owner)
+                self._set_entity_owner(self._entity_owner)
                 Notifier.publish(event)
                 Notifier.publish(event)
             else:
             else:
                 self._pending_changes.pop(key, None)
                 self._pending_changes.pop(key, None)
                 self._pending_deletions.add(key)
                 self._pending_deletions.add(key)
                 self._entity_owner._in_context_attributes_changed_collector.append(event)
                 self._entity_owner._in_context_attributes_changed_collector.append(event)
+
+    def _set_entity_owner(self, entity_owner):
+        from ... import core as tp
+
+        tp.set(entity_owner)

+ 3 - 4
taipy/core/data/csv.py

@@ -20,7 +20,6 @@ from taipy.common.config.common.scope import Scope
 
 
 from .._entity._reload import _Reloader
 from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
-from ..job.job_id import JobId
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from .data_node import DataNode
 from .data_node import DataNode
@@ -116,16 +115,16 @@ class CSVDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
     def storage_type(cls) -> str:
     def storage_type(cls) -> str:
         return cls.__STORAGE_TYPE
         return cls.__STORAGE_TYPE
 
 
-    def write_with_column_names(self, data: Any, columns: Optional[List[str]] = None, job_id: Optional[JobId] = None):
+    def write_with_column_names(self, data: Any, columns: Optional[List[str]] = None, editor_id: Optional[str] = None):
         """Write a selection of columns.
         """Write a selection of columns.
 
 
         Arguments:
         Arguments:
             data (Any): The data to write.
             data (Any): The data to write.
             columns (Optional[List[str]]): The list of column names to write.
             columns (Optional[List[str]]): The list of column names to write.
-            job_id (JobId): An optional identifier of the writer.
+            editor_id (str): An optional identifier of the writer.
         """
         """
         self._write(data, columns)
         self._write(data, columns)
-        self.track_edit(timestamp=datetime.now(), job_id=job_id)
+        self.track_edit(editor_id=editor_id, timestamp=datetime.now())
 
 
     def _read(self):
     def _read(self):
         return self._read_from_path()
         return self._read_from_path()

+ 38 - 17
taipy/core/data/data_node.py

@@ -11,6 +11,7 @@
 
 
 import functools
 import functools
 import os
 import os
+import typing
 import uuid
 import uuid
 from abc import abstractmethod
 from abc import abstractmethod
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
@@ -33,7 +34,7 @@ from ..job.job_id import JobId
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten
 from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten
 from ._filter import _FilterDataNode
 from ._filter import _FilterDataNode
-from .data_node_id import DataNodeId, Edit
+from .data_node_id import EDIT_COMMENT_KEY, EDIT_EDITOR_ID_KEY, EDIT_JOB_ID_KEY, EDIT_TIMESTAMP_KEY, DataNodeId, Edit
 from .operator import JoinOperator
 from .operator import JoinOperator
 
 
 
 
@@ -200,6 +201,11 @@ class DataNode(_Entity, _Labeled):
         """
         """
         return self._edits
         return self._edits
 
 
+    @edits.setter  # type: ignore
+    @_self_setter(_MANAGER_NAME)
+    def edits(self, val):
+        self._edits = val
+
     @property  # type: ignore
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     @_self_reload(_MANAGER_NAME)
     def last_edit_date(self) -> Optional[datetime]:
     def last_edit_date(self) -> Optional[datetime]:
@@ -415,29 +421,29 @@ class DataNode(_Entity, _Labeled):
             )
             )
             return None
             return None
 
 
-    def append(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]):
+    def append(self, data, editor_id: Optional[str] = None, **kwargs: Any):
         """Append some data to this data node.
         """Append some data to this data node.
 
 
         Arguments:
         Arguments:
             data (Any): The data to write to this data node.
             data (Any): The data to write to this data node.
-            job_id (JobId): An optional identifier of the writer.
-            **kwargs (dict[str, any]): Extra information to attach to the edit document
+            editor_id (str): An optional identifier of the editor.
+            **kwargs (Any): Extra information to attach to the edit document
                 corresponding to this write.
                 corresponding to this write.
         """
         """
         from ._data_manager_factory import _DataManagerFactory
         from ._data_manager_factory import _DataManagerFactory
 
 
         self._append(data)
         self._append(data)
-        self.track_edit(job_id=job_id, **kwargs)
+        self.track_edit(editor_id=editor_id, **kwargs)
         self.unlock_edit()
         self.unlock_edit()
         _DataManagerFactory._build_manager()._set(self)
         _DataManagerFactory._build_manager()._set(self)
 
 
-    def write(self, data, job_id: Optional[JobId] = None, **kwargs: Dict[str, Any]):
+    def write(self, data, job_id: Optional[JobId] = None, **kwargs: Any):
         """Write some data to this data node.
         """Write some data to this data node.
 
 
         Arguments:
         Arguments:
             data (Any): The data to write to this data node.
             data (Any): The data to write to this data node.
-            job_id (JobId): An optional identifier of the writer.
-            **kwargs (dict[str, any]): Extra information to attach to the edit document
+            job_id (JobId): An optional identifier of the job writing the data.
+            **kwargs (Any): Extra information to attach to the edit document
                 corresponding to this write.
                 corresponding to this write.
         """
         """
         from ._data_manager_factory import _DataManagerFactory
         from ._data_manager_factory import _DataManagerFactory
@@ -447,20 +453,35 @@ class DataNode(_Entity, _Labeled):
         self.unlock_edit()
         self.unlock_edit()
         _DataManagerFactory._build_manager()._set(self)
         _DataManagerFactory._build_manager()._set(self)
 
 
-    def track_edit(self, **options):
+    def track_edit(self,
+                   job_id: Optional[str] = None,
+                   editor_id: Optional[str] = None,
+                   timestamp: Optional[datetime] = None,
+                   comment: Optional[str] = None,
+                   **options: Any):
         """Creates and adds a new entry in the edits attribute without writing the data.
         """Creates and adds a new entry in the edits attribute without writing the data.
 
 
         Arguments:
         Arguments:
-            options (dict[str, any]): track `timestamp`, `comments`, `job_id`. The others are user-custom, users can
-                use options to attach any information to an external edit of a data node.
+            job_id (Optional[str]): The optional identifier of the job writing the data.
+            editor_id (Optional[str]): The optional identifier of the editor writing the data.
+            timestamp (Optional[datetime]): The optional timestamp of the edit. If not provided, the
+                current time is used.
+            comment (Optional[str]): The optional comment of the edit.
+            **options (Any): User-custom attributes to attach to the edit.
         """
         """
         edit = {k: v for k, v in options.items() if v is not None}
         edit = {k: v for k, v in options.items() if v is not None}
-        if "timestamp" not in edit:
-            edit["timestamp"] = (
-                self._get_last_modified_datetime(self._properties.get(self._PATH_KEY, None)) or datetime.now()
-            )
-        self.last_edit_date = edit.get("timestamp")
-        self._edits.append(edit)
+        if job_id:
+            edit[EDIT_JOB_ID_KEY] = job_id
+        if editor_id:
+            edit[EDIT_EDITOR_ID_KEY] = editor_id
+        if comment:
+            edit[EDIT_COMMENT_KEY] = comment
+        if not timestamp:
+            timestamp = self._get_last_modified_datetime(self._properties.get(self._PATH_KEY)) or datetime.now()
+        edit[EDIT_TIMESTAMP_KEY] = timestamp
+        self.last_edit_date = edit.get(EDIT_TIMESTAMP_KEY)
+        self._edits.append(typing.cast(Edit, edit))
+        self.edits = self._edits
 
 
     def lock_edit(self, editor_id: Optional[str] = None):
     def lock_edit(self, editor_id: Optional[str] = None):
         """Lock the data node modification.
         """Lock the data node modification.

+ 4 - 0
taipy/core/data/data_node_id.py

@@ -17,3 +17,7 @@ DataNodeId.__doc__ = """Type that holds a `DataNode^` identifier."""
 Edit = NewType("Edit", Dict[str, Any])
 Edit = NewType("Edit", Dict[str, Any])
 """Type that holds a `DataNode^` edit information."""
 """Type that holds a `DataNode^` edit information."""
 Edit.__doc__ = """Type that holds a `DataNode^` edit information."""
 Edit.__doc__ = """Type that holds a `DataNode^` edit information."""
+EDIT_TIMESTAMP_KEY = "timestamp"
+EDIT_JOB_ID_KEY = "job_id"
+EDIT_COMMENT_KEY = "comment"
+EDIT_EDITOR_ID_KEY = "editor_id"

+ 3 - 4
taipy/core/data/excel.py

@@ -21,7 +21,6 @@ from taipy.common.config.common.scope import Scope
 from .._entity._reload import _Reloader
 from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..exceptions.exceptions import ExposedTypeLengthMismatch, NonExistingExcelSheet, SheetNameLengthMismatch
 from ..exceptions.exceptions import ExposedTypeLengthMismatch, NonExistingExcelSheet, SheetNameLengthMismatch
-from ..job.job_id import JobId
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from .data_node import DataNode
 from .data_node import DataNode
@@ -119,13 +118,13 @@ class ExcelDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
         """Return the storage type of the data node: "excel"."""
         """Return the storage type of the data node: "excel"."""
         return cls.__STORAGE_TYPE
         return cls.__STORAGE_TYPE
 
 
-    def write_with_column_names(self, data: Any, columns: List[str] = None, job_id: Optional[JobId] = None) -> None:
+    def write_with_column_names(self, data: Any, columns: List[str] = None, editor_id: Optional[str] = None) -> None:
         """Write a set of columns.
         """Write a set of columns.
 
 
         Arguments:
         Arguments:
             data (Any): The data to write.
             data (Any): The data to write.
             columns (List[str]): The list of column names to write.
             columns (List[str]): The list of column names to write.
-            job_id (Optional[JobId]): An optional identifier of the writer.
+            editor_id (Optional[str]): An optional identifier of the writer.
         """
         """
         if isinstance(data, Dict) and all(isinstance(x, (pd.DataFrame, np.ndarray)) for x in data.values()):
         if isinstance(data, Dict) and all(isinstance(x, (pd.DataFrame, np.ndarray)) for x in data.values()):
             self._write_excel_with_multiple_sheets(data, columns=columns)
             self._write_excel_with_multiple_sheets(data, columns=columns)
@@ -134,7 +133,7 @@ class ExcelDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
             if columns:
             if columns:
                 df = self._set_column_if_dataframe(df, columns)
                 df = self._set_column_if_dataframe(df, columns)
             self._write_excel_with_single_sheet(df.to_excel, self.path, index=False)
             self._write_excel_with_single_sheet(df.to_excel, self.path, index=False)
-        self.track_edit(timestamp=datetime.now(), job_id=job_id)
+        self.track_edit(timestamp=datetime.now(), editor_id=editor_id)
 
 
     @staticmethod
     @staticmethod
     def _check_exposed_type(exposed_type):
     def _check_exposed_type(exposed_type):

+ 3 - 4
taipy/core/data/parquet.py

@@ -21,7 +21,6 @@ from taipy.common.config.common.scope import Scope
 from .._entity._reload import _Reloader
 from .._entity._reload import _Reloader
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..exceptions.exceptions import UnknownCompressionAlgorithm, UnknownParquetEngine
 from ..exceptions.exceptions import UnknownCompressionAlgorithm, UnknownParquetEngine
-from ..job.job_id import JobId
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._file_datanode_mixin import _FileDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from ._tabular_datanode_mixin import _TabularDataNodeMixin
 from .data_node import DataNode
 from .data_node import DataNode
@@ -163,14 +162,14 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
         """Return the storage type of the data node: "parquet"."""
         """Return the storage type of the data node: "parquet"."""
         return cls.__STORAGE_TYPE
         return cls.__STORAGE_TYPE
 
 
-    def _write_with_kwargs(self, data: Any, job_id: Optional[JobId] = None, **write_kwargs):
+    def _write_with_kwargs(self, data: Any, editor_id: Optional[str] = None, **write_kwargs):
         """Write the data referenced by this data node.
         """Write the data referenced by this data node.
 
 
         Keyword arguments here which are also present in the Data Node config will overwrite them.
         Keyword arguments here which are also present in the Data Node config will overwrite them.
 
 
         Arguments:
         Arguments:
             data (Any): The data to write.
             data (Any): The data to write.
-            job_id (JobId): An optional identifier of the writer.
+            editor_id (str): An optional identifier of the writer.
             **write_kwargs (dict[str, any]): The keyword arguments passed to the function
             **write_kwargs (dict[str, any]): The keyword arguments passed to the function
                 `pandas.DataFrame.to_parquet()`.
                 `pandas.DataFrame.to_parquet()`.
         """
         """
@@ -189,7 +188,7 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
         # Ensure that the columns are strings, otherwise writing will fail with pandas 1.3.5
         # Ensure that the columns are strings, otherwise writing will fail with pandas 1.3.5
         df.columns = df.columns.astype(str)
         df.columns = df.columns.astype(str)
         df.to_parquet(self._path, **kwargs)
         df.to_parquet(self._path, **kwargs)
-        self.track_edit(timestamp=datetime.now(), job_id=job_id)
+        self.track_edit(timestamp=datetime.now(), editor_id=editor_id)
 
 
     def read_with_kwargs(self, **read_kwargs):
     def read_with_kwargs(self, **read_kwargs):
         """Read data from this data node.
         """Read data from this data node.

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

@@ -626,7 +626,6 @@ class _Factory:
                 ("hover_text", PropertyType.dynamic_string),
                 ("hover_text", PropertyType.dynamic_string),
                 ("label",),
                 ("label",),
                 ("value_by_id", PropertyType.boolean),
                 ("value_by_id", PropertyType.boolean),
-                ("unselected_value", PropertyType.string, ""),
                 ("allow_unselect", PropertyType.boolean),
                 ("allow_unselect", PropertyType.boolean),
                 ("on_change", PropertyType.function),
                 ("on_change", PropertyType.function),
                 ("mode",),
                 ("mode",),

+ 1 - 1
taipy/gui/gui.py

@@ -735,7 +735,7 @@ class Gui:
         elif rel_var and isinstance(current_value, _TaipyLovValue):  # pragma: no cover
         elif rel_var and isinstance(current_value, _TaipyLovValue):  # pragma: no cover
             lov_holder = _getscopeattr_drill(self, self.__evaluator.get_hash_from_expr(rel_var))
             lov_holder = _getscopeattr_drill(self, self.__evaluator.get_hash_from_expr(rel_var))
             if isinstance(lov_holder, _TaipyLov):
             if isinstance(lov_holder, _TaipyLov):
-                if value:
+                if isinstance(value, str):
                     val = value if isinstance(value, list) else [value]
                     val = value if isinstance(value, list) else [value]
                     elt_4_ids = self.__adapter._get_elt_per_ids(lov_holder.get_name(), lov_holder.get())
                     elt_4_ids = self.__adapter._get_elt_per_ids(lov_holder.get_name(), lov_holder.get())
                     ret_val = [elt_4_ids.get(x, x) for x in val]
                     ret_val = [elt_4_ids.get(x, x) for x in val]

+ 3 - 9
taipy/gui/viselements.json

@@ -275,12 +275,6 @@
                         "default_value": "False",
                         "default_value": "False",
                         "doc": "If set, this allows de-selection and the value is set to unselected_value."
                         "doc": "If set, this allows de-selection and the value is set to unselected_value."
                     },
                     },
-                    {
-                        "name": "unselected_value",
-                        "type": "Any",
-                        "default_value": "None",
-                        "doc": "Value assigned to <i>value</i> when no item is selected."
-                    },
                     {
                     {
                         "name": "mode",
                         "name": "mode",
                         "type": "str",
                         "type": "str",
@@ -1632,7 +1626,7 @@
             }
             }
         ],
         ],
         [
         [
-            "alert",  
+            "alert",
             {
             {
                 "inherits": ["shared"],
                 "inherits": ["shared"],
                 "properties": [
                 "properties": [
@@ -1662,7 +1656,7 @@
                         "doc": "If False, the alert is hidden."
                         "doc": "If False, the alert is hidden."
                     }
                     }
                 ]
                 ]
-            }                       
+            }
         ],
         ],
         [
         [
             "status",
             "status",
@@ -1842,7 +1836,7 @@
                     }
                     }
                 ]
                 ]
             }
             }
-        ]                                       
+        ]
     ],
     ],
     "blocks": [
     "blocks": [
         [
         [

+ 5 - 4
taipy/gui_core/_context.py

@@ -52,6 +52,7 @@ from taipy.core import delete as core_delete
 from taipy.core import get as core_get
 from taipy.core import get as core_get
 from taipy.core import submit as core_submit
 from taipy.core import submit as core_submit
 from taipy.core.data._file_datanode_mixin import _FileDataNodeMixin
 from taipy.core.data._file_datanode_mixin import _FileDataNodeMixin
+from taipy.core.data.data_node_id import EDIT_COMMENT_KEY, EDIT_EDITOR_ID_KEY, EDIT_JOB_ID_KEY, EDIT_TIMESTAMP_KEY
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
 from taipy.core.notification.notifier import Notifier
@@ -993,7 +994,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):
             res = []
             res = []
             for e in dn.edits:
             for e in dn.edits:
-                job_id = e.get("job_id")
+                job_id = e.get(EDIT_JOB_ID_KEY)
                 job: t.Optional[Job] = None
                 job: t.Optional[Job] = None
                 if job_id:
                 if job_id:
                     if not (reason := is_readable(job_id)):
                     if not (reason := is_readable(job_id)):
@@ -1002,11 +1003,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         job = core_get(job_id)
                         job = core_get(job_id)
                 res.append(
                 res.append(
                     (
                     (
-                        e.get("timestamp"),
-                        job_id if job_id else e.get("writer_identifier", ""),
+                        e.get(EDIT_TIMESTAMP_KEY),
+                        job_id if job_id else e.get(EDIT_EDITOR_ID_KEY, ""),
                         f"Execution of task {job.task.get_simple_label()}."
                         f"Execution of task {job.task.get_simple_label()}."
                         if job and job.task
                         if job and job.task
-                        else e.get("comment", ""),
+                        else e.get(EDIT_COMMENT_KEY, ""),
                     )
                     )
                 )
                 )
             return sorted(res, key=lambda r: r[0], reverse=True)
             return sorted(res, key=lambda r: r[0], reverse=True)

+ 64 - 2
tests/core/data/test_data_node.py

@@ -23,7 +23,13 @@ from taipy.common.config.exceptions.exceptions import InvalidConfigurationId
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.data_node import DataNode
 from taipy.core.data.data_node import DataNode
-from taipy.core.data.data_node_id import DataNodeId
+from taipy.core.data.data_node_id import (
+    EDIT_COMMENT_KEY,
+    EDIT_EDITOR_ID_KEY,
+    EDIT_JOB_ID_KEY,
+    EDIT_TIMESTAMP_KEY,
+    DataNodeId,
+)
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from taipy.core.exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from taipy.core.job.job_id import JobId
 from taipy.core.job.job_id import JobId
@@ -667,7 +673,7 @@ class TestDataNode:
         data_node.path = "baz.p"
         data_node.path = "baz.p"
         assert data_node.path == "baz.p"
         assert data_node.path == "baz.p"
 
 
-    def test_track_edit(self):
+    def test_edit_edit_tracking(self):
         dn_config = Config.configure_data_node("A")
         dn_config = Config.configure_data_node("A")
         data_node = _DataManager._bulk_get_or_create([dn_config])[dn_config]
         data_node = _DataManager._bulk_get_or_create([dn_config])[dn_config]
 
 
@@ -745,3 +751,59 @@ class TestDataNode:
         # This new syntax will be the only one allowed: https://github.com/Avaiga/taipy-core/issues/806
         # This new syntax will be the only one allowed: https://github.com/Avaiga/taipy-core/issues/806
         dn.properties["name"] = "baz"
         dn.properties["name"] = "baz"
         assert dn.name == "baz"
         assert dn.name == "baz"
+
+    def test_track_edit(self):
+        dn_config = Config.configure_data_node("A")
+        data_node = _DataManager._bulk_get_or_create([dn_config])[dn_config]
+
+        before = datetime.now()
+        data_node.track_edit(job_id="job_1")
+        data_node.track_edit(editor_id="editor_1")
+        data_node.track_edit(comment="This is a comment on this edit")
+        data_node.track_edit(editor_id="editor_2", comment="This is another comment on this edit")
+        data_node.track_edit(editor_id="editor_3", foo="bar")
+        after = datetime.now()
+        timestamp = datetime.now()
+        data_node.track_edit(timestamp=timestamp)
+        _DataManagerFactory._build_manager()._set(data_node)
+        # To save the edits because track edit does not save the data node
+
+        assert len(data_node.edits) == 6
+        assert data_node.edits[-1] == data_node.get_last_edit()
+        assert data_node.last_edit_date == data_node.get_last_edit().get(EDIT_TIMESTAMP_KEY)
+
+        edit_0 = data_node.edits[0]
+        assert len(edit_0) == 2
+        assert edit_0[EDIT_JOB_ID_KEY] == "job_1"
+        assert edit_0[EDIT_TIMESTAMP_KEY] >= before
+        assert edit_0[EDIT_TIMESTAMP_KEY] <= after
+
+        edit_1 = data_node.edits[1]
+        assert len(edit_1) == 2
+        assert edit_1[EDIT_EDITOR_ID_KEY] == "editor_1"
+        assert edit_1[EDIT_TIMESTAMP_KEY] >= before
+        assert edit_1[EDIT_TIMESTAMP_KEY] <= after
+
+        edit_2 = data_node.edits[2]
+        assert len(edit_2) == 2
+        assert edit_2[EDIT_COMMENT_KEY] == "This is a comment on this edit"
+        assert edit_2[EDIT_TIMESTAMP_KEY] >= before
+        assert edit_2[EDIT_TIMESTAMP_KEY] <= after
+
+        edit_3 = data_node.edits[3]
+        assert len(edit_3) == 3
+        assert edit_3[EDIT_EDITOR_ID_KEY] == "editor_2"
+        assert edit_3[EDIT_COMMENT_KEY] == "This is another comment on this edit"
+        assert edit_3[EDIT_TIMESTAMP_KEY] >= before
+        assert edit_3[EDIT_TIMESTAMP_KEY] <= after
+
+        edit_4 = data_node.edits[4]
+        assert len(edit_4) == 3
+        assert edit_4[EDIT_EDITOR_ID_KEY] == "editor_3"
+        assert edit_4["foo"] == "bar"
+        assert edit_4[EDIT_TIMESTAMP_KEY] >= before
+        assert edit_4[EDIT_TIMESTAMP_KEY] <= after
+
+        edit_5 = data_node.edits[5]
+        assert len(edit_5) == 1
+        assert edit_5[EDIT_TIMESTAMP_KEY] == timestamp

+ 9 - 8
tests/core/notification/test_events_published.py

@@ -145,13 +145,13 @@ def test_events_published_for_writing_dn():
     all_evts = RecordingConsumer(register_id_0, register_queue_0)
     all_evts = RecordingConsumer(register_id_0, register_queue_0)
     all_evts.start()
     all_evts.start()
 
 
-    # Write input manually trigger 4 data node update events
-    # for last_edit_date, editor_id, editor_expiration_date and edit_in_progress
+    # Write input manually trigger 5 data node update events
+    # for last_edit_date, editor_id, editor_expiration_date, edit_in_progress and edits
     scenario.the_input.write("test")
     scenario.the_input.write("test")
     snapshot = all_evts.capture()
     snapshot = all_evts.capture()
-    assert len(snapshot.collected_events) == 4
-    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 4
-    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 4
+    assert len(snapshot.collected_events) == 5
+    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 5
+    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 5
     all_evts.stop()
     all_evts.stop()
 
 
 
 
@@ -178,22 +178,23 @@ def test_events_published_for_scenario_submission():
     # 1 submission update event for is_completed
     # 1 submission update event for is_completed
     scenario.submit()
     scenario.submit()
     snapshot = all_evts.capture()
     snapshot = all_evts.capture()
-    assert len(snapshot.collected_events) == 17
+    assert len(snapshot.collected_events) == 18
     assert snapshot.entity_type_collected.get(EventEntityType.CYCLE, 0) == 0
     assert snapshot.entity_type_collected.get(EventEntityType.CYCLE, 0) == 0
-    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 7
+    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 8
     assert snapshot.entity_type_collected.get(EventEntityType.TASK, 0) == 0
     assert snapshot.entity_type_collected.get(EventEntityType.TASK, 0) == 0
     assert snapshot.entity_type_collected.get(EventEntityType.SEQUENCE, 0) == 0
     assert snapshot.entity_type_collected.get(EventEntityType.SEQUENCE, 0) == 0
     assert snapshot.entity_type_collected.get(EventEntityType.SCENARIO, 0) == 1
     assert snapshot.entity_type_collected.get(EventEntityType.SCENARIO, 0) == 1
     assert snapshot.entity_type_collected.get(EventEntityType.JOB, 0) == 4
     assert snapshot.entity_type_collected.get(EventEntityType.JOB, 0) == 4
     assert snapshot.entity_type_collected.get(EventEntityType.SUBMISSION, 0) == 5
     assert snapshot.entity_type_collected.get(EventEntityType.SUBMISSION, 0) == 5
     assert snapshot.operation_collected.get(EventOperation.CREATION, 0) == 2
     assert snapshot.operation_collected.get(EventOperation.CREATION, 0) == 2
-    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 14
+    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 15
     assert snapshot.operation_collected.get(EventOperation.SUBMISSION, 0) == 1
     assert snapshot.operation_collected.get(EventOperation.SUBMISSION, 0) == 1
 
 
     assert snapshot.attr_name_collected["last_edit_date"] == 1
     assert snapshot.attr_name_collected["last_edit_date"] == 1
     assert snapshot.attr_name_collected["editor_id"] == 2
     assert snapshot.attr_name_collected["editor_id"] == 2
     assert snapshot.attr_name_collected["editor_expiration_date"] == 2
     assert snapshot.attr_name_collected["editor_expiration_date"] == 2
     assert snapshot.attr_name_collected["edit_in_progress"] == 2
     assert snapshot.attr_name_collected["edit_in_progress"] == 2
+    assert snapshot.attr_name_collected["edits"] == 1
     assert snapshot.attr_name_collected["status"] == 3
     assert snapshot.attr_name_collected["status"] == 3
     assert snapshot.attr_name_collected["jobs"] == 1
     assert snapshot.attr_name_collected["jobs"] == 1
     assert snapshot.attr_name_collected["submission_status"] == 3
     assert snapshot.attr_name_collected["submission_status"] == 3

+ 9 - 9
tests/core/notification/test_published_ready_to_run_event.py

@@ -61,9 +61,9 @@ def test_write_never_written_input_does_not_publish_submittable_event():
     snapshot = all_evts.capture()
     snapshot = all_evts.capture()
 
 
     # Since it is a lazy property, no submittable event is published. Only the data node update events are published.
     # Since it is a lazy property, no submittable event is published. Only the data node update events are published.
-    assert len(snapshot.collected_events) == 4
-    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 4
-    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 4
+    assert len(snapshot.collected_events) == 5
+    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 5
+    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 5
 
 
 
 
 def test_write_never_written_input_publish_submittable_event_if_scenario_in_property():
 def test_write_never_written_input_publish_submittable_event_if_scenario_in_property():
@@ -85,12 +85,12 @@ def test_write_never_written_input_publish_submittable_event_if_scenario_in_prop
     snapshot = all_evts.capture()
     snapshot = all_evts.capture()
 
 
     # Since it is a lazy property, no submittable event is published. Only the data node update events are published.
     # Since it is a lazy property, no submittable event is published. Only the data node update events are published.
-    assert len(snapshot.collected_events) == 13
-    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 7
+    assert len(snapshot.collected_events) == 14
+    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 8
     assert snapshot.entity_type_collected.get(EventEntityType.TASK, 0) == 2
     assert snapshot.entity_type_collected.get(EventEntityType.TASK, 0) == 2
     assert snapshot.entity_type_collected.get(EventEntityType.SEQUENCE, 0) == 2
     assert snapshot.entity_type_collected.get(EventEntityType.SEQUENCE, 0) == 2
     assert snapshot.entity_type_collected.get(EventEntityType.SCENARIO, 0) == 2
     assert snapshot.entity_type_collected.get(EventEntityType.SCENARIO, 0) == 2
-    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 13
+    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 14
     assert snapshot.attr_name_collected["is_submittable"] == 6
     assert snapshot.attr_name_collected["is_submittable"] == 6
     assert snapshot.attr_value_collected["is_submittable"] == [False, False, False, True, True, True]
     assert snapshot.attr_value_collected["is_submittable"] == [False, False, False, True, True, True]
 
 
@@ -109,9 +109,9 @@ def test_write_output_does_not_publish_submittable_event():
     scenario.dn_2.write(15)
     scenario.dn_2.write(15)
     snapshot = all_evts.capture()
     snapshot = all_evts.capture()
 
 
-    assert len(snapshot.collected_events) == 4
-    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 4
-    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 4
+    assert len(snapshot.collected_events) == 5
+    assert snapshot.entity_type_collected.get(EventEntityType.DATA_NODE, 0) == 5
+    assert snapshot.operation_collected.get(EventOperation.UPDATE, 0) == 5
     assert "is_submittable" not in snapshot.attr_name_collected
     assert "is_submittable" not in snapshot.attr_name_collected
     assert "is_submittable" not in snapshot.attr_value_collected
     assert "is_submittable" not in snapshot.attr_value_collected
     all_evts.stop()
     all_evts.stop()

+ 2 - 3
tests/gui/builder/control/test_toggle.py

@@ -16,14 +16,14 @@ from taipy.gui import Gui
 def test_toggle_builder(gui: Gui, helpers):
 def test_toggle_builder(gui: Gui, helpers):
     with tgb.Page(frame=None) as page:
     with tgb.Page(frame=None) as page:
         tgb.toggle(theme=True)  # type: ignore[attr-defined]
         tgb.toggle(theme=True)  # type: ignore[attr-defined]
-    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"']
     helpers.test_control_builder(gui, page, expected_list)
     helpers.test_control_builder(gui, page, expected_list)
 
 
 
 
 def test_toggle_allow_unselected_builder(gui: Gui, helpers):
 def test_toggle_allow_unselected_builder(gui: Gui, helpers):
     with tgb.Page(frame=None) as page:
     with tgb.Page(frame=None) as page:
         tgb.toggle(allow_unselect=True, lov="1;2")  # type: ignore[attr-defined]
         tgb.toggle(allow_unselect=True, lov="1;2")  # type: ignore[attr-defined]
-    expected_list = ["<Toggle", 'unselectedValue=""', "allowUnselect={true}"]
+    expected_list = ["<Toggle", "allowUnselect={true}"]
     helpers.test_control_builder(gui, page, expected_list)
     helpers.test_control_builder(gui, page, expected_list)
 
 
 
 
@@ -40,7 +40,6 @@ def test_toggle_lov_builder(gui: Gui, test_client, helpers):
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
-        'unselectedValue=""',
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
     ]
     ]
     helpers.test_control_builder(gui, page, expected_list)
     helpers.test_control_builder(gui, page, expected_list)

+ 4 - 6
tests/gui/control/test_toggle.py

@@ -14,19 +14,19 @@ from taipy.gui import Gui
 
 
 def test_toggle_md(gui: Gui, helpers):
 def test_toggle_md(gui: Gui, helpers):
     md_string = "<|toggle|theme|>"
     md_string = "<|toggle|theme|>"
-    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"']
     helpers.test_control_md(gui, md_string, expected_list)
     helpers.test_control_md(gui, md_string, expected_list)
 
 
 
 
 def test_toggle_width_md(gui: Gui, helpers):
 def test_toggle_width_md(gui: Gui, helpers):
     md_string = "<|toggle|theme|width=70%|>"
     md_string = "<|toggle|theme|width=70%|>"
-    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""', 'width="70%"']
+    expected_list = ["<Toggle", 'mode="theme"', 'width="70%"']
     helpers.test_control_md(gui, md_string, expected_list)
     helpers.test_control_md(gui, md_string, expected_list)
 
 
 
 
 def test_toggle_allow_unselected_md(gui: Gui, helpers):
 def test_toggle_allow_unselected_md(gui: Gui, helpers):
     md_string = "<|toggle|lov=1;2|allow_unselect|>"
     md_string = "<|toggle|lov=1;2|allow_unselect|>"
-    expected_list = ["<Toggle", 'unselectedValue=""', "allowUnselect={true}"]
+    expected_list = ["<Toggle", "allowUnselect={true}"]
     helpers.test_control_md(gui, md_string, expected_list)
     helpers.test_control_md(gui, md_string, expected_list)
 
 
 
 
@@ -42,7 +42,6 @@ def test_toggle_lov_md(gui: Gui, test_client, helpers):
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
-        'unselectedValue=""',
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
     ]
     ]
     helpers.test_control_md(gui, md_string, expected_list)
     helpers.test_control_md(gui, md_string, expected_list)
@@ -50,7 +49,7 @@ def test_toggle_lov_md(gui: Gui, test_client, helpers):
 
 
 def test_toggle_html_1(gui: Gui, helpers):
 def test_toggle_html_1(gui: Gui, helpers):
     html_string = '<taipy:toggle theme="True" />'
     html_string = '<taipy:toggle theme="True" />'
-    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"']
     helpers.test_control_html(gui, html_string, expected_list)
     helpers.test_control_html(gui, html_string, expected_list)
 
 
 
 
@@ -66,7 +65,6 @@ def test_toggle_html_2(gui: Gui, test_client, helpers):
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         "lov={_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0}",
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVars="lov=_TpL_tp_TpExPr_gui_get_adapted_lov_lov_tuple_TPMDL_0_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
         'updateVarName="_TpLv_tpec_TpExPr_x_TPMDL_0"',
-        'unselectedValue=""',
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
     ]
     ]
     helpers.test_control_html(gui, html_string, expected_list)
     helpers.test_control_html(gui, html_string, expected_list)