Преглед на файлове

Merge branch 'develop' into test/StatusList

Nam Nguyen преди 8 месеца
родител
ревизия
ab17835cc9
променени са 100 файла, в които са добавени 1776 реда и са изтрити 830 реда
  1. 4 3
      .github/workflows/build-and-release-single-package.yml
  2. 5 7
      .github/workflows/build-and-release.yml
  3. 1 1
      .github/workflows/trigger-benchmark.yml
  4. 1 1
      Pipfile
  5. 22 23
      README.md
  6. 56 57
      frontend/taipy-gui/package-lock.json
  7. 7 0
      frontend/taipy-gui/public/stylekit/utilities/misc.css
  8. 28 23
      frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
  9. 25 8
      frontend/taipy-gui/src/components/Taipy/Button.spec.tsx
  10. 13 3
      frontend/taipy-gui/src/components/Taipy/Button.tsx
  11. 2 5
      frontend/taipy-gui/src/components/Taipy/Chat.tsx
  12. 21 3
      frontend/taipy-gui/src/components/Taipy/DateRange.spec.tsx
  13. 30 12
      frontend/taipy-gui/src/components/Taipy/DateRange.tsx
  14. 28 2
      frontend/taipy-gui/src/components/Taipy/DateSelector.spec.tsx
  15. 11 5
      frontend/taipy-gui/src/components/Taipy/DateSelector.tsx
  16. 28 14
      frontend/taipy-gui/src/components/Taipy/Field.spec.tsx
  17. 19 4
      frontend/taipy-gui/src/components/Taipy/Field.tsx
  18. 44 19
      frontend/taipy-gui/src/components/Taipy/FileDownload.spec.tsx
  19. 12 2
      frontend/taipy-gui/src/components/Taipy/FileDownload.tsx
  20. 19 9
      frontend/taipy-gui/src/components/Taipy/FileSelector.spec.tsx
  21. 7 4
      frontend/taipy-gui/src/components/Taipy/FileSelector.tsx
  22. 14 0
      frontend/taipy-gui/src/components/Taipy/Input.spec.tsx
  23. 73 42
      frontend/taipy-gui/src/components/Taipy/Input.tsx
  24. 2 14
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx
  25. 25 22
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
  26. 69 31
      frontend/taipy-gui/src/components/Taipy/ThemeToggle.spec.tsx
  27. 17 6
      frontend/taipy-gui/src/components/Taipy/ThemeToggle.tsx
  28. 37 8
      frontend/taipy-gui/src/components/Taipy/Toggle.spec.tsx
  29. 22 9
      frontend/taipy-gui/src/components/Taipy/Toggle.tsx
  30. 5 6
      frontend/taipy-gui/src/components/Taipy/utils.ts
  31. 4 2
      frontend/taipy-gui/src/utils/ErrorBoundary.tsx
  32. 80 91
      frontend/taipy/package-lock.json
  33. 35 30
      frontend/taipy/src/CoreSelector.tsx
  34. 2 1
      frontend/taipy/src/DataNodeTable.tsx
  35. 20 46
      frontend/taipy/src/DataNodeViewer.tsx
  36. 118 15
      frontend/taipy/src/JobSelector.tsx
  37. 186 0
      frontend/taipy/src/JobViewer.tsx
  38. 20 16
      frontend/taipy/src/utils.ts
  39. 9 4
      taipy/config/config.py
  40. 0 3
      taipy/config/pyproject.toml
  41. 1 1
      taipy/config/version.json
  42. 1 1
      taipy/core/common/warn_if_inputs_not_ready.py
  43. 35 0
      taipy/core/config/checkers/_scenario_config_checker.py
  44. 0 6
      taipy/core/cycle/cycle.py
  45. 2 8
      taipy/core/data/data_node.py
  46. 1 1
      taipy/core/data/parquet.py
  47. 7 0
      taipy/core/exceptions/exceptions.py
  48. 0 3
      taipy/core/pyproject.toml
  49. 3 10
      taipy/core/scenario/_scenario_manager.py
  50. 23 10
      taipy/core/scenario/scenario.py
  51. 13 4
      taipy/core/sequence/sequence.py
  52. 2 4
      taipy/core/taipy.py
  53. 13 3
      taipy/core/task/task.py
  54. 1 1
      taipy/core/version.json
  55. 1 1
      taipy/gui/_renderers/builder.py
  56. 8 0
      taipy/gui/_renderers/factory.py
  57. 0 3
      taipy/gui/pyproject.toml
  58. 1 1
      taipy/gui/version.json
  59. 70 17
      taipy/gui/viselements.json
  60. 9 1
      taipy/gui_core/_GuiCoreLib.py
  61. 112 71
      taipy/gui_core/_context.py
  62. 27 7
      taipy/gui_core/viselements.json
  63. 0 3
      taipy/rest/pyproject.toml
  64. 1 1
      taipy/rest/version.json
  65. 0 3
      taipy/templates/pyproject.toml
  66. 1 1
      taipy/templates/version.json
  67. 1 1
      taipy/version.json
  68. 69 11
      tests/core/config/checkers/test_scenario_config_checker.py
  69. 5 4
      tests/core/cycle/test_cycle.py
  70. 1 1
      tests/core/cycle/test_cycle_manager.py
  71. 4 4
      tests/core/data/test_csv_data_node.py
  72. 2 2
      tests/core/data/test_data_manager.py
  73. 0 1
      tests/core/data/test_data_node.py
  74. 18 18
      tests/core/data/test_excel_data_node.py
  75. 0 1
      tests/core/data/test_json_data_node.py
  76. 4 4
      tests/core/data/test_parquet_data_node.py
  77. 1 1
      tests/core/data/test_read_excel_data_node.py
  78. 5 5
      tests/core/data/test_sql_data_node.py
  79. 3 3
      tests/core/data/test_sql_table_data_node.py
  80. 26 7
      tests/core/scenario/test_scenario.py
  81. 22 22
      tests/core/scenario/test_scenario_manager.py
  82. 0 0
      tests/core/scenario/test_scenario_manager_with_sql_repo.py
  83. 23 6
      tests/core/sequence/test_sequence.py
  84. 1 1
      tests/core/sequence/test_sequence_manager.py
  85. 17 1
      tests/core/task/test_task.py
  86. 8 0
      tests/gui/control/test_button.py
  87. 13 0
      tests/gui/control/test_date.py
  88. 15 0
      tests/gui/control/test_date_range.py
  89. 12 0
      tests/gui/control/test_file_download.py
  90. 13 0
      tests/gui/control/test_file_selector.py
  91. 17 1
      tests/gui/control/test_input.py
  92. 6 0
      tests/gui/control/test_number.py
  93. 7 0
      tests/gui/control/test_text.py
  94. 6 0
      tests/gui/control/test_toggle.py
  95. 2 2
      tests/gui_core/test_context_is_deletable.py
  96. 8 8
      tests/gui_core/test_context_is_editable.py
  97. 2 2
      tests/gui_core/test_context_is_promotable.py
  98. 10 10
      tests/gui_core/test_context_is_readable.py
  99. 1 1
      tools/packages/pipfiles/Pipfile3.10.max
  100. 1 1
      tools/packages/pipfiles/Pipfile3.11.max

+ 4 - 3
.github/workflows/build-and-release-single-package.yml

@@ -51,8 +51,6 @@ jobs:
           ${{ github.event.inputs.target_package }} >> $GITHUB_OUTPUT
 
   build-and-release-package:
-    permissions:
-      contents: write
     needs: [fetch-versions]
     timeout-minutes: 20
     runs-on: ubuntu-latest
@@ -159,6 +157,9 @@ jobs:
         working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
           python -m build
+
+      - name: Rename files
+        run: |
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
       - name: Create tag and release
@@ -175,7 +176,7 @@ jobs:
 
       - name: Ensure Taipy release is marked as latest
         run: |
-          gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
+           gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 5 - 7
.github/workflows/build-and-release.yml

@@ -44,12 +44,6 @@ jobs:
         run: |
           python tools/release/setup_version.py ALL ${{ github.event.inputs.release_type }} ${{ github.event.inputs.target_version }} ${{ steps.extract_branch.outputs.branch }} >> $GITHUB_OUTPUT
 
-      - uses: stefanzweifel/git-auto-commit-action@v4
-        with:
-          commit_message: Update version to ${{ steps.version-setup.outputs.NEW_VERSION }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
   build-and-release-taipy-packages:
     needs: [fetch-versions]
     timeout-minutes: 20
@@ -177,7 +171,7 @@ jobs:
 
   build-and-release-taipy:
     runs-on: ubuntu-latest
-    needs: [build-and-release-taipy-packages, fetch-versions ]
+    needs: [build-and-release-taipy-packages, fetch-versions]
     timeout-minutes: 20
     steps:
       - uses: actions/checkout@v4
@@ -218,6 +212,10 @@ jobs:
         run: |
           cp -r tools/packages/taipy/. .
 
+      - name: Build Frontend
+        run: |
+          python tools/frontend/bundle_build.py
+
       - name: Build Taipy package
         run: |
           python -m build

+ 1 - 1
.github/workflows/trigger-benchmark.yml

@@ -7,7 +7,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - name: Trigger taipy-integration-testing
+      - name: Trigger taipy-benchmark computation
         uses: peter-evans/repository-dispatch@v1
         with:
           token: ${{secrets.TAIPY_INTEGRATION_TESTING_ACCESS_TOKEN}}

+ 1 - 1
Pipfile

@@ -31,7 +31,7 @@ pytz = "==2021.3"
 simple-websocket = "==0.10.1"
 sqlalchemy = "==2.0.16"
 toml = "==0.10"
-twisted = "==23.8.0"
+twisted = "==24.7.0"
 tzlocal = "==3.0"
 boto3 = "==1.29.1"
 watchdog = "==4.0.0"

+ 22 - 23
README.md

@@ -1,5 +1,3 @@
-[![Taipy Designer banner](https://github.com/Avaiga/taipy/assets/31435778/6378ffd4-438a-498f-9385-10394f7d53fb)](https://links.taipy.io/306TwUH)
-
 <div align="center">
   <a href="https://taipy.io?utm_source=github" target="_blank">
   <picture>
@@ -14,7 +12,8 @@ Build Python Data & AI web applications
 </h1>
 
 <div align="center">
-From simple pilots to production-ready web applications in no time. No more compromise on performance, customization, and scalability.
+From simple pilots to production-ready web applications in no time. <br />
+No more compromise on performance, customization, and scalability.
 </div>
 
 <br />
@@ -29,9 +28,9 @@ From simple pilots to production-ready web applications in no time. No more comp
     <br />
     <a href="https://docs.taipy.io/en/latest/"><strong>📚 Explore the docs </strong></a>
     <br />
-    <a href="https://discord.com/invite/SJyz2VJGxV">  🫱🏼‍🫲🏼 Discord support</a>
+    <a href="https://discord.com/invite/SJyz2VJGxV"><strong>  🫱🏼‍🫲🏼 Discord support </strong></a>
     <br />
-    <a href="https://docs.taipy.io/en/latest/gallery/"> 👀 Demos & Examples</a>
+    <a href="https://docs.taipy.io/en/latest/gallery/"><strong> 👀 Demos & Examples </strong></a>
   </p>
 
 &nbsp;
@@ -41,9 +40,9 @@ From simple pilots to production-ready web applications in no time. No more comp
 Taipy is designed for data scientists and machine learning engineers to build data & AI web applications.
 &nbsp;
 
-⭐️ Enables building production-ready web applications. `<br />`
-⭐️ No need to learn new languages. Only Python is needed.`<br />`
-⭐️ Concentrate on Data and AI algorithms without development and deployment complexities.
+⭐️ Enables building production-ready web applications. <br />
+⭐️ No need to learn new languages. Only Python is needed.<br />
+⭐️ Concentrate on Data and AI algorithms without development and deployment complexities.<br />
 
 &nbsp;
 
@@ -55,11 +54,11 @@ Taipy is a Two-in-One Tool for UI Generation and Scenario/Data Management
 
 | User Interface Generation                                                                       | Scenario and Data Management                                                                        |
 | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
-| `<img src="readme_img/taipy_github_GUI_video.gif" alt="Interface Animation"  width="100%" />` | `<img src="readme_img/taipy_github_scenarios_video.gif" alt="Back-End Animation"  width="100%"/>` |
+| <img src="readme_img/taipy_github_GUI_video.gif" alt="Interface Animation"  width="100%" /> | <img src="readme_img/taipy_github_scenarios_video.gif" alt="Back-End Animation"  width="100%"/> |
 
 &nbsp;
 
-## ✨ Features
+## ✨ Key Features
 
 <img src="readme_img/taipy_github_scenario.png" alt="Scenario Banner"  width="49%" />  <img src="readme_img/taipy-github-optimized.png" alt="Back-End Animation"  width="49.7%"/>
 <img src="readme_img/taipy_github_data_support.png" alt="Back-End Animation"  width="49.7%" />
@@ -74,21 +73,21 @@ To install Taipy stable release run:
 pip install taipy
 ```
 
-To install Taipy on a Conda Environment or from source, please refer to the [Installation Guide](https://docs.taipy.io/en/latest/installation/).`<br />`
+To install Taipy on a Conda Environment or from source, please refer to the [Installation Guide](https://docs.taipy.io/en/latest/installation/).<br />
 To get started with Taipy, please refer to the [Getting Started Guide](https://docs.taipy.io/en/latest/getting_started/).
 
 &nbsp;
 
 ## 🔌 Scenario and Data Management
 
-Let's create a scenario in Taipy that allows you to filter movie data based on your chosen genre.`<br />`
-This scenario is designed as a straightforward pipeline.`<br />`
-Every time you change your genre selection, the scenario runs to process your request.`<br />`
+Let's create a scenario in Taipy that allows you to filter movie data based on your chosen genre.<br />
+This scenario is designed as a straightforward pipeline.<br />
+Every time you change your genre selection, the scenario runs to process your request.<br />
 It then displays the top seven most popular movies in that genre.
 
 <br />
 
-> ⚠️ Keep in mind, in this example, we're using a very basic pipeline that consists of just one task. However,`<br />`
+> ⚠️ Keep in mind, in this example, we're using a very basic pipeline that consists of just one task. However,<br />
 > Taipy is capable of handling much more complex pipelines 🚀
 
 <br />
@@ -110,11 +109,11 @@ This is the execution graph of the scenario we are implementing
 
 ### Taipy Studio
 
-You can use the Taipy Studio extension in Visual Studio Code to configure your scenario with no code`<br />`
-Your configuration is automatically saved as a TOML file.`<br />`
+You can use the Taipy Studio extension in Visual Studio Code to configure your scenario with no code<br />
+Your configuration is automatically saved as a TOML file.<br />
 Check out Taipy Studio [Documentation](https://docs.taipy.io/en/latest/manuals/studio/)
 
-For more advanced use cases or if you prefer coding your configurations instead of using Taipy Studio,`<br />`
+For more advanced use cases or if you prefer coding your configurations instead of using Taipy Studio,<br />
 Check out the movie genre demo scenario creation with this [Demo](https://docs.taipy.io/en/latest/gallery/other/movie_genre_selector/).
 
 ![TaipyStudio](https://github.com/Avaiga/taipy/raw/develop/readme_img/readme_demo_studio.gif)
@@ -123,7 +122,7 @@ Check out the movie genre demo scenario creation with this [Demo](https://docs.t
 
 ## User Interface Generation and Scenario & Data Management
 
-This simple Taipy application demonstrates how to create a basic film recommendation system using Taipy.`<br />`
+This simple Taipy application demonstrates how to create a basic film recommendation system using Taipy.<br />
 The application filters a dataset of films based on the user's selected genre and displays the top seven films in that genre by popularity.
 Here is the full code for both the frontend and backend of the application.
 
@@ -192,17 +191,17 @@ if __name__ == "__main__":
 ```
 
 And the final result:
-`<img src="readme_img/readme_app.gif" />`
+<img src="readme_img/readme_app.gif" />
 
 &nbsp;
 
 ## ⚒️ Contributing
 
-Want to help build Taipy? Check out our [Contributing Guide](https://github.com/Avaiga/taipy/blob/develop/CONTRIBUTING.md).
+Want to help build Taipy? Check out our [**Contributing Guide**](https://github.com/Avaiga/taipy/blob/develop/CONTRIBUTING.md).
 
 ## 🪄 Code of conduct
 
-Want to be part of the Taipy community? Check out our [Code of Conduct](https://github.com/Avaiga/taipy/blob/develop/CODE_OF_CONDUCT.md)
+Want to be part of the Taipy community? Check out our **[Code of Conduct](https://github.com/Avaiga/taipy/blob/develop/CODE_OF_CONDUCT.md)**
 
 ## 🪪 License
 
@@ -210,7 +209,7 @@ Copyright 2021-2024 Avaiga Private Limited
 
 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 the License. You may obtain a copy of the License at
-[http://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+(Apache License)[http://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
 
 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the

+ 56 - 57
frontend/taipy-gui/package-lock.json

@@ -191,9 +191,9 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.4.tgz",
-      "integrity": "sha512-NFtZmZsyzDPJnk9Zg3BbTfKKc9UlHYzD0E//p2Z3B9nCwwtJW9T0gVbCz8+fBngnn4zf1Dr3IK8PHQQHq0lDQw==",
+      "version": "7.25.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
+      "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
       "dependencies": {
         "@babel/types": "^7.25.4",
         "@jridgewell/gen-mapping": "^0.3.5",
@@ -2097,14 +2097,14 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.13.0.tgz",
-      "integrity": "sha512-cmpAfkzOjUgL4I8WenU4elm1QJO8vWpGmIPCezT3Q9wFjGL1QApQhJ5gMZ+X4tM6Gha9AhIWNQX5eXHKbSoyFQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.14.0.tgz",
+      "integrity": "sha512-3xI3xYVxqPU4//KfE4FcR+Zs7UT4kkDPvA+IDOcQdRUyVwmcXCjBuJZgKgJMqSCNK/KIJZQQrpmy5XGHOKTbdA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -2161,12 +2161,12 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.13.0.tgz",
-      "integrity": "sha512-eUK7iykkDWU+wBfTzE/S0qh4awgVgsORfrpvuPbUp+E6qUj1Xhu9M/WKzbwz0CPFnTJZwBQ9KYrxpGXnPBEpRQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.14.0.tgz",
+      "integrity": "sha512-+qWIHLgt2vgH6bKmf7IwRvS86UbZRWKAdDY/yTQJaqzCzyesUvQhD+WRxe1kpdCK8UE061S9/Ju7hLkM4kjRNA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/utils": "^5.16.5"
+        "@mui/utils": "^5.16.6"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -2180,15 +2180,15 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.13.0.tgz",
-      "integrity": "sha512-ADixvp85a0iZ7AOzBuCPQ+yl+gMq0BlIWhg3GfbX+57sMhjcdOEUUxcGIcIt6pw1V05bVXE2/QP+5qzDamiGPw==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.14.0.tgz",
+      "integrity": "sha512-j1sK0tLrsiCu0FxwTJQkVm2nbLYc1tRLwmPDAXcQ3nuzGDzn0x/IA28dBjxse/+oNy4j2cpJz3k/mSz/a4ZLjA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@mui/x-internals": "7.13.0",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@mui/x-internals": "7.14.0",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -2490,13 +2490,12 @@
       }
     },
     "node_modules/@testing-library/jest-dom": {
-      "version": "6.4.8",
-      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz",
-      "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==",
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz",
+      "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==",
       "dev": true,
       "dependencies": {
         "@adobe/css-tools": "^4.4.0",
-        "@babel/runtime": "^7.9.2",
         "aria-query": "^5.0.0",
         "chalk": "^3.0.0",
         "css.escape": "^1.5.1",
@@ -2760,9 +2759,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.11",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz",
-      "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==",
+      "version": "8.56.12",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+      "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
       "dev": true,
       "dependencies": {
         "@types/estree": "*",
@@ -3950,9 +3949,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.7.4",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
-      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
+      "version": "1.7.5",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
+      "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
       "dependencies": {
         "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
@@ -4340,9 +4339,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001651",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
-      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "version": "1.0.30001653",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
+      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
       "funding": [
         {
           "type": "opencollective",
@@ -4544,9 +4543,9 @@
       }
     },
     "node_modules/cjs-module-lexer": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz",
-      "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz",
+      "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==",
       "dev": true
     },
     "node_modules/clamp": {
@@ -5957,9 +5956,9 @@
       }
     },
     "node_modules/emoji-regex": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
-      "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+      "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
       "dev": true
     },
     "node_modules/end-of-stream": {
@@ -12041,9 +12040,9 @@
       ]
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "dependencies": {
         "braces": "^3.0.3",
@@ -15015,20 +15014,20 @@
       }
     },
     "node_modules/ts-jest": {
-      "version": "29.2.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz",
-      "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==",
+      "version": "29.2.5",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
+      "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==",
       "dev": true,
       "dependencies": {
-        "bs-logger": "0.x",
+        "bs-logger": "^0.2.6",
         "ejs": "^3.1.10",
-        "fast-json-stable-stringify": "2.x",
+        "fast-json-stable-stringify": "^2.1.0",
         "jest-util": "^29.0.0",
         "json5": "^2.2.3",
-        "lodash.memoize": "4.x",
-        "make-error": "1.x",
-        "semver": "^7.5.3",
-        "yargs-parser": "^21.0.1"
+        "lodash.memoize": "^4.1.2",
+        "make-error": "^1.3.6",
+        "semver": "^7.6.3",
+        "yargs-parser": "^21.1.1"
       },
       "bin": {
         "ts-jest": "cli.js"
@@ -15162,9 +15161,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
     },
     "node_modules/type": {
       "version": "2.7.3",
@@ -15314,9 +15313,9 @@
       }
     },
     "node_modules/typedoc-plugin-markdown": {
-      "version": "4.2.5",
-      "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.5.tgz",
-      "integrity": "sha512-ZWIfc0OqwEtQfuaqbmM1kesMi/Fhc++W+5f3TDEm1Tsi28pHSoZk4WCOm4lNuN30WtEImwAHhhXC4DIWki1DiA==",
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.2.6.tgz",
+      "integrity": "sha512-k33o2lZSGpL3GjH28eW+RsujzCYFP0L5GNqpK+wa4CBcMOxpj8WV7SydNRLS6eSa2UvaPvNVJTaAZ6Tm+8GXoA==",
       "dev": true,
       "engines": {
         "node": ">= 18"

+ 7 - 0
frontend/taipy-gui/public/stylekit/utilities/misc.css

@@ -25,6 +25,13 @@
 
 .d-flex {
     display: flex !important;
+
+}
+
+.d-flex > .taipy-part:not(.MuiDrawer-docked) {
+    width: -webkit-fill-available;
+    width: -moz-available;
+    /* width: fill-available; */
 }
 
 .d-block {

+ 28 - 23
frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

@@ -228,7 +228,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             const nr = newValue.data as RowType[];
             if (Array.isArray(nr) && nr.length > newValue.start) {
                 setRows(nr);
-                newValue.comp && setCompRows(newValue.comp as RowType[])
+                newValue.comp && setCompRows(newValue.comp as RowType[]);
                 promise && promise.resolve();
             } else {
                 promise && promise.reject();
@@ -247,7 +247,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                 setOrder(isAsc ? "desc" : "asc");
                 setOrderBy(col);
                 setRows([]);
-                setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
+                Promise.resolve().then(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true)); // So that the state can be changed
             }
         },
         [orderBy, order]
@@ -256,7 +256,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
     useEffect(() => {
         if (refresh) {
             setRows([]);
-            setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
+            Promise.resolve().then(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true)); // So that the state can be changed
         }
     }, [refresh]);
 
@@ -279,34 +279,36 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         if (baseColumns) {
             try {
                 let filter = false;
-                Object.values(baseColumns).forEach((col) => {
-                    if (typeof col.filter != "boolean") {
-                        col.filter = !!props.filter;
+                const newCols: Record<string, ColumnDesc> = {};
+                Object.entries(baseColumns).forEach(([cId, cDesc]) => {
+                    const nDesc = (newCols[cId] = { ...cDesc });
+                    if (typeof nDesc.filter != "boolean") {
+                        nDesc.filter = !!props.filter;
                     }
-                    filter = filter || col.filter;
-                    if (typeof col.notEditable != "boolean") {
-                        col.notEditable = !editable;
+                    filter = filter || nDesc.filter;
+                    if (typeof nDesc.notEditable != "boolean") {
+                        nDesc.notEditable = !editable;
                     }
-                    if (col.tooltip === undefined) {
-                        col.tooltip = props.tooltip;
+                    if (nDesc.tooltip === undefined) {
+                        nDesc.tooltip = props.tooltip;
                     }
                 });
                 addDeleteColumn(
                     (active && editable && (onAdd || onDelete) ? 1 : 0) +
                         (active && filter ? 1 : 0) +
                         (active && downloadable ? 1 : 0),
-                    baseColumns
+                    newCols
                 );
-                const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
+                const colsOrder = Object.keys(newCols).sort(getsortByIndex(newCols));
                 const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
-                    if (baseColumns[col].style) {
+                    if (newCols[col].style) {
                         pv.styles = pv.styles || {};
-                        pv.styles[baseColumns[col].dfid] = baseColumns[col].style as string;
+                        pv.styles[newCols[col].dfid] = newCols[col].style as string;
                     }
-                    hNan = hNan || !!baseColumns[col].nanValue;
-                    if (baseColumns[col].tooltip) {
+                    hNan = hNan || !!newCols[col].nanValue;
+                    if (newCols[col].tooltip) {
                         pv.tooltips = pv.tooltips || {};
-                        pv.tooltips[baseColumns[col].dfid] = baseColumns[col].tooltip as string;
+                        pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
                     }
                     return pv;
                 }, {});
@@ -314,7 +316,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                     styTt.styles = styTt.styles || {};
                     styTt.styles[LINE_STYLE] = props.lineStyle;
                 }
-                return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
+                return [colsOrder, newCols, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
                 console.info("ATable.columns: " + ((e as Error).message || e));
             }
@@ -407,7 +409,9 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                         afs,
                         compare ? onCompare : undefined,
                         updateVars && getUpdateVar(updateVars, "comparedatas"),
-                        typeof userData == "object" ? (userData as Record<string, Record<string, unknown>>).context : undefined
+                        typeof userData == "object"
+                            ? (userData as Record<string, Record<string, unknown>>).context
+                            : undefined
                     )
                 );
             });
@@ -429,7 +433,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             onCompare,
             dispatch,
             module,
-            userData
+            userData,
         ]
     );
 
@@ -537,7 +541,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             selection: selected,
             formatConfig: formatConfig,
             onValidation: active && onEdit ? onCellValidation : undefined,
-            onDeletion: active && onDelete ? onRowDeletion : undefined,
+            onDeletion: active && editable && onDelete ? onRowDeletion : undefined,
             onRowSelection: active && onAction ? onRowSelection : undefined,
             onRowClick: active && onAction ? onRowClick : undefined,
             lineStyle: props.lineStyle,
@@ -553,6 +557,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             columns,
             selected,
             formatConfig,
+            editable,
             onEdit,
             onCellValidation,
             onDelete,
@@ -584,7 +589,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [
-                                                    active && onAdd ? (
+                                                    active && editable && onAdd ? (
                                                         <Tooltip title="Add a row" key="addARow">
                                                             <IconButton
                                                                 onClick={onAddRowClick}

+ 25 - 8
frontend/taipy-gui/src/components/Taipy/Button.spec.tsx

@@ -32,17 +32,28 @@ describe("Button Component", () => {
         expect(elt).toHaveClass("taipy-button");
     });
     it("displays the default value", async () => {
-        const { getByText } = render(
-            <Button defaultLabel="titi" label={undefined as unknown as string}  />
-        );
+        const { getByText } = render(<Button defaultLabel="titi" label={undefined as unknown as string} />);
         getByText("titi");
     });
     it("displays an image", async () => {
         const { getByAltText } = render(
-            <Button defaultLabel={JSON.stringify({path: "/image/fred.png", text: "fred"})} label={undefined as unknown as string} />
+            <Button
+                defaultLabel={JSON.stringify({ path: "/image/fred.png", text: "fred" })}
+                label={undefined as unknown as string}
+            />
         );
         const img = getByAltText("fred");
-        expect(img.tagName).toBe("IMG")
+        expect(img.tagName).toBe("IMG");
+    });
+    it("displays with width=70%", async () => {
+        const { getByText } = render(<Button label="toto" width="70%" />);
+        const element = getByText("toto");
+        expect(element).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        const { getByText } = render(<Button label="toto" width={500} />);
+        const element = getByText("toto");
+        expect(element).toHaveStyle("width: 500px");
     });
     it("is disabled", async () => {
         const { getByText } = render(<Button label="val" active={false} />);
@@ -62,11 +73,17 @@ describe("Button Component", () => {
     it("dispatch a well formed message", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByText } = render(<TaipyContext.Provider value={{ state, dispatch }}>
+        const { getByText } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
                 <Button label="Button" onAction="on_action" />
-            </TaipyContext.Provider>);
+            </TaipyContext.Provider>
+        );
         const elt = getByText("Button");
         await userEvent.click(elt);
-        expect(dispatch).toHaveBeenCalledWith({"name": "", "payload": {args: [], action: "on_action"}, "type": "SEND_ACTION_ACTION"});
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "",
+            payload: { args: [], action: "on_action" },
+            type: "SEND_ACTION_ACTION",
+        });
     });
 });

+ 13 - 3
frontend/taipy-gui/src/components/Taipy/Button.tsx

@@ -11,13 +11,13 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useEffect, useCallback, useMemo } from "react";
 import CardHeader from "@mui/material/CardHeader";
 import MuiButton from "@mui/material/Button";
 import Tooltip from "@mui/material/Tooltip";
 
 import { createSendActionNameAction } from "../../context/taipyReducers";
-import { getSuffixedClassNames, TaipyActiveProps } from "./utils";
+import { getCssSize, getSuffixedClassNames, TaipyActiveProps } from "./utils";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
 import { stringIcon, Icon, IconAvatar } from "../../utils/icon";
 
@@ -25,6 +25,7 @@ interface ButtonProps extends TaipyActiveProps {
     onAction?: string;
     label: string;
     defaultLabel?: string;
+    width?: string | number;
 }
 
 const cardSx = { p: 0 };
@@ -39,6 +40,8 @@ const Button = (props: ButtonProps) => {
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
 
+    const buttonSx = useMemo(() => (props.width ? { width: getCssSize(props.width) } : undefined), [props.width]);
+
     const handleClick = useCallback(() => {
         dispatch(createSendActionNameAction(id, module, onAction));
     }, [id, onAction, dispatch, module]);
@@ -61,7 +64,14 @@ const Button = (props: ButtonProps) => {
 
     return (
         <Tooltip title={hover || ""}>
-            <MuiButton id={id} variant="outlined" className={className} onClick={handleClick} disabled={!active}>
+            <MuiButton
+                id={id}
+                variant="outlined"
+                className={className}
+                onClick={handleClick}
+                disabled={!active}
+                sx={buttonSx}
+            >
                 {typeof value === "string" ? (
                     value
                 ) : (value as Icon).text ? (

+ 2 - 5
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -305,10 +305,7 @@ const Chat = (props: ChatProps) => {
                 if (Array.isArray(nr) && nr.length > newValue.start && nr[newValue.start]) {
                     setRows((old) => {
                         old.length && nr.length > old.length && setShowMessage(true);
-                        if (nr.length < old.length) {
-                            return nr.concat(old.slice(nr.length));
-                        }
-                        if (old.length > newValue.start) {
+                        if (newValue.start > 0 && old.length > newValue.start) {
                             return old.slice(0, newValue.start).concat(nr.slice(newValue.start));
                         }
                         return nr;
@@ -332,7 +329,7 @@ const Chat = (props: ChatProps) => {
 
     useEffect(() => {
         if (refresh) {
-            setTimeout(() => loadMoreItems(0), 1); // So that the state can be changed
+            Promise.resolve().then(() => loadMoreItems(0)); // So that the state can be changed
         }
     }, [refresh, loadMoreItems]);
 

+ 21 - 3
frontend/taipy-gui/src/components/Taipy/DateRange.spec.tsx

@@ -156,6 +156,24 @@ describe("DateRange Component", () => {
         const endInput = getByLabelText("end") as HTMLInputElement;
         expect(endInput.value).toBe("01/31/2001");
     });
+    it("displays with width=70%", async () => {
+        render(
+            <LocalizationProvider dateAdapter={AdapterDateFns}>
+                <DateRange dates={curDates} width="70%" />
+            </LocalizationProvider>
+        );
+        const elt = document.querySelector(".MuiStack-root");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        render(
+            <LocalizationProvider dateAdapter={AdapterDateFns}>
+                <DateRange dates={curDates} width={500} />
+            </LocalizationProvider>
+        );
+        const elt = document.querySelector(".MuiStack-root");
+        expect(elt).toHaveStyle("width: 500px");
+    });
     it("is disabled", async () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
@@ -256,7 +274,7 @@ describe("DateRange with time Component", () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
                 <DateRange
-                    defaultDates="[&quot;2001-01-01T00:00:01.001Z&quot;,&quot;2001-01-31T00:00:01.001Z&quot;]"
+                    defaultDates='["2001-01-01T00:00:01.001Z","2001-01-31T00:00:01.001Z"]'
                     withTime={true}
                     dates={undefined as unknown as string[]}
                     className="tp-dt"
@@ -274,7 +292,7 @@ describe("DateRange with time Component", () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
                 <DateRange
-                    defaultDates="[&quot;2001-01-01T00:10:01.001Z&quot;,&quot;2001-01-31T00:11:01.001Z&quot;]"
+                    defaultDates='["2001-01-01T00:10:01.001Z","2001-01-31T00:11:01.001Z"]'
                     withTime={true}
                     dates={undefined as unknown as string[]}
                     className="tp-dt"
@@ -293,7 +311,7 @@ describe("DateRange with time Component", () => {
         const { getByLabelText } = render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
                 <DateRange
-                    defaultDates="[&quot;2001-01-01T00:00:01.001Z&quot;,&quot;2001-01-31T00:00:01.001Z&quot;]"
+                    defaultDates='["2001-01-01T00:00:01.001Z","2001-01-31T00:00:01.001Z"]'
                     dates={undefined as unknown as string[]}
                     withTime={true}
                     className="taipy-date-range"

+ 30 - 12
frontend/taipy-gui/src/components/Taipy/DateRange.tsx

@@ -11,9 +11,10 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useState, useEffect, useCallback } from "react";
-import Box from "@mui/material/Box";
+import React, { useState, useEffect, useCallback, useMemo } from "react";
+import Stack from "@mui/material/Stack";
 import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
 import { DatePicker, DatePickerProps } from "@mui/x-date-pickers/DatePicker";
 import { BaseDateTimePickerSlotProps } from "@mui/x-date-pickers/DateTimePicker/shared";
 import { DateTimePicker, DateTimePickerProps } from "@mui/x-date-pickers/DateTimePicker";
@@ -21,7 +22,7 @@ import { isValid } from "date-fns";
 import { ErrorBoundary } from "react-error-boundary";
 
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps } from "./utils";
+import { getCssSize, getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps } from "./utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import Field from "./Field";
@@ -36,9 +37,10 @@ interface DateRangeProps extends TaipyActiveProps, TaipyChangeProps {
     editable?: boolean;
     labelStart?: string;
     labelEnd?: string;
+    separator?: string;
+    width?: string | number;
 }
 
-const boxSx = { display: "inline-flex", alignItems: "center", gap: "0.5em" };
 const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotProps<Date>;
 
 const getRangeDateTime = (
@@ -61,7 +63,7 @@ const getRangeDateTime = (
 };
 
 const DateRange = (props: DateRangeProps) => {
-    const { updateVarName, withTime = false, id, propagate = true } = props;
+    const { updateVarName, withTime = false, id, propagate = true, separator = "-" } = props;
     const dispatch = useDispatch();
     const formatConfig = useFormatConfig();
     const tz = formatConfig.timeZone;
@@ -75,6 +77,8 @@ const DateRange = (props: DateRangeProps) => {
     const editable = useDynamicProperty(props.editable, props.defaultEditable, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
 
+    const dateSx = useMemo(() => (props.width ? { maxWidth: "100%" } : undefined), [props.width]);
+
     const handleChange = useCallback(
         (v: Date | null, start: boolean) => {
             setValue((dates) => {
@@ -124,7 +128,15 @@ const DateRange = (props: DateRangeProps) => {
     return (
         <ErrorBoundary FallbackComponent={ErrorFallback}>
             <Tooltip title={hover || ""}>
-                <Box id={id} className={className} sx={boxSx}>
+                <Stack
+                    id={id}
+                    className={className}
+                    gap={0.5}
+                    direction="row"
+                    display="inline-flex"
+                    alignItems="center"
+                    width={props.width ? getCssSize(props.width) : undefined}
+                >
                     {editable ? (
                         withTime ? (
                             <>
@@ -141,8 +153,9 @@ const DateRange = (props: DateRangeProps) => {
                                     slotProps={textFieldProps}
                                     label={props.labelStart}
                                     format={props.format}
+                                    sx={dateSx}
                                 />
-                                -
+                                <Typography>{separator}</Typography>
                                 <DateTimePicker
                                     {...(endProps as DateTimePickerProps<Date>)}
                                     value={value[1]}
@@ -156,6 +169,7 @@ const DateRange = (props: DateRangeProps) => {
                                     slotProps={textFieldProps}
                                     label={props.labelEnd}
                                     format={props.format}
+                                    sx={dateSx}
                                 />
                             </>
                         ) : (
@@ -173,8 +187,9 @@ const DateRange = (props: DateRangeProps) => {
                                     slotProps={textFieldProps}
                                     label={props.labelStart}
                                     format={props.format}
+                                    sx={dateSx}
                                 />
-                                -
+                                <Typography>{separator}</Typography>
                                 <DatePicker
                                     {...(endProps as DatePickerProps<Date>)}
                                     value={value[1]}
@@ -188,6 +203,7 @@ const DateRange = (props: DateRangeProps) => {
                                     slotProps={textFieldProps}
                                     label={props.labelEnd}
                                     format={props.format}
+                                    sx={dateSx}
                                 />
                             </>
                         )
@@ -195,22 +211,24 @@ const DateRange = (props: DateRangeProps) => {
                         <>
                             <Field
                                 dataType="datetime"
-                                value={props.dates[0]}
+                                value={value[0] && isValid(value[0]) ? value[0].toISOString() : ""}
                                 format={props.format}
                                 id={id && id + "-field"}
                                 className={getSuffixedClassNames(className, "-text")}
+                                width={props.width && "100%"}
                             />
-                            -
+                            <Typography>{separator}</Typography>
                             <Field
                                 dataType="datetime"
-                                value={props.dates[1]}
+                                value={value[1] && isValid(value[1]) ? value[1].toISOString() : ""}
                                 format={props.format}
                                 id={id && id + "-field"}
                                 className={getSuffixedClassNames(className, "-text")}
+                                width={props.width && "100%"}
                             />
                         </>
                     )}
-                </Box>
+                </Stack>
             </Tooltip>
         </ErrorBoundary>
     );

+ 28 - 2
frontend/taipy-gui/src/components/Taipy/DateSelector.spec.tsx

@@ -99,7 +99,11 @@ describe("DateSelector Component", () => {
     it("displays the default value with format", async () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
-                <DateSelector defaultDate="2011-01-01T00:00:01.001Z" date={undefined as unknown as string} format="yy-MM-dd" />
+                <DateSelector
+                    defaultDate="2011-01-01T00:00:01.001Z"
+                    date={undefined as unknown as string}
+                    format="yy-MM-dd"
+                />
             </LocalizationProvider>
         );
         const input = document.querySelector("input");
@@ -120,6 +124,24 @@ describe("DateSelector Component", () => {
         const input = getByLabelText("a label") as HTMLInputElement;
         expect(input.value).toBe("01/01/2001");
     });
+    it("displays with width=70%", async () => {
+        render(
+            <LocalizationProvider dateAdapter={AdapterDateFns}>
+                <DateSelector date={curDateStr} width="70%" />
+            </LocalizationProvider>
+        );
+        const elt = document.querySelector(".MuiFormControl-root");
+        expect(elt).toHaveStyle("max-width: 70%");
+    });
+    it("displays with width=500", async () => {
+        render(
+            <LocalizationProvider dateAdapter={AdapterDateFns}>
+                <DateSelector date={curDateStr} width={500} />
+            </LocalizationProvider>
+        );
+        const elt = document.querySelector(".MuiFormControl-root");
+        expect(elt).toHaveStyle("max-width: 500px");
+    });
     it("is disabled", async () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
@@ -212,7 +234,11 @@ describe("DateSelector with time Component", () => {
     it("displays the default value with format", async () => {
         render(
             <LocalizationProvider dateAdapter={AdapterDateFns}>
-                <DateSelector defaultDate="2011-01-01T00:10:01.001Z" date={undefined as unknown as string} format="yy-MM-dd mm" />
+                <DateSelector
+                    defaultDate="2011-01-01T00:10:01.001Z"
+                    date={undefined as unknown as string}
+                    format="yy-MM-dd mm"
+                />
             </LocalizationProvider>
         );
         const input = document.querySelector("input");

+ 11 - 5
frontend/taipy-gui/src/components/Taipy/DateSelector.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useEffect, useCallback, useMemo } from "react";
 import Box from "@mui/material/Box";
 import Tooltip from "@mui/material/Tooltip";
 import { DatePicker, DatePickerProps } from "@mui/x-date-pickers/DatePicker";
@@ -21,7 +21,7 @@ import { isValid } from "date-fns";
 import { ErrorBoundary } from "react-error-boundary";
 
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps } from "./utils";
+import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps, getCssSize } from "./utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import Field from "./Field";
@@ -39,6 +39,7 @@ interface DateSelectorProps extends TaipyActiveProps, TaipyChangeProps {
     defaultEditable?: boolean;
     editable?: boolean;
     label?: string;
+    width?: string | number;
 }
 
 const boxSx = { display: "inline-block" };
@@ -61,6 +62,8 @@ const DateSelector = (props: DateSelectorProps) => {
     const min = useDynamicProperty(props.min, props.defaultMin, undefined);
     const max = useDynamicProperty(props.max, props.defaultMax, undefined);
 
+    const dateSx = useMemo(() => (props.width ? { maxWidth: getCssSize(props.width) } : undefined), [props.width]);
+
     const handleChange = useCallback(
         (v: Date | null) => {
             setValue(v);
@@ -72,12 +75,12 @@ const DateSelector = (props: DateSelectorProps) => {
                         dateToString(newDate, withTime),
                         module,
                         props.onChange,
-                        propagate,
-                    ),
+                        propagate
+                    )
                 );
             }
         },
-        [updateVarName, dispatch, withTime, propagate, tz, props.onChange, module],
+        [updateVarName, dispatch, withTime, propagate, tz, props.onChange, module]
     );
 
     // Run every time props.value get updated
@@ -115,6 +118,7 @@ const DateSelector = (props: DateSelectorProps) => {
                                 slotProps={textFieldProps}
                                 label={props.label}
                                 format={props.format}
+                                sx={dateSx}
                             />
                         ) : (
                             <DatePicker
@@ -127,6 +131,7 @@ const DateSelector = (props: DateSelectorProps) => {
                                 slotProps={textFieldProps}
                                 label={props.label}
                                 format={props.format}
+                                sx={dateSx}
                             />
                         )
                     ) : (
@@ -137,6 +142,7 @@ const DateSelector = (props: DateSelectorProps) => {
                             format={props.format}
                             id={id && id + "-field"}
                             className={getSuffixedClassNames(className, "-text")}
+                            width={props.width}
                         />
                     )}
                 </Box>

+ 28 - 14
frontend/taipy-gui/src/components/Taipy/Field.spec.tsx

@@ -12,38 +12,52 @@
  */
 
 import React from "react";
-import {render} from "@testing-library/react";
+import { render } from "@testing-library/react";
 import "@testing-library/jest-dom";
 
-import Field from './Field';
+import Field from "./Field";
 
 describe("Field Component", () => {
     it("renders", async () => {
-        const {getByText} = render(<Field value="toto" />);
+        const { getByText } = render(<Field value="toto" />);
         const elt = getByText("toto");
         expect(elt.tagName).toBe("SPAN");
-    })
+    });
     it("displays the right info for string", async () => {
-        const {getByText} = render(<Field value="toto" defaultValue="titi" className="taipy-field" />);
+        const { getByText } = render(<Field value="toto" defaultValue="titi" className="taipy-field" />);
         const elt = getByText("toto");
         expect(elt).toHaveClass("taipy-field");
-    })
+    });
     it("displays the default value", async () => {
-        const {getByText} = render(<Field defaultValue="titi" value={undefined as unknown as string} />);
+        const { getByText } = render(<Field defaultValue="titi" value={undefined as unknown as string} />);
         getByText("titi");
-    })
+    });
     it("displays a date with format", async () => {
         const myDate = new Date();
         myDate.setMonth(1, 1);
-        const {getByText} = render(<Field defaultValue="titi" value={myDate.toISOString()} dataType="datetime" format="MM/dd" /> );
+        const { getByText } = render(
+            <Field defaultValue="titi" value={myDate.toISOString()} dataType="datetime" format="MM/dd" />
+        );
         getByText("02/01");
-    })
+    });
     it("displays a int with format", async () => {
-        const {getByText} = render(<Field defaultValue="titi" value={12} dataType="int" format="%.2f" /> );
+        const { getByText } = render(<Field defaultValue="titi" value={12} dataType="int" format="%.2f" />);
         getByText("12.00");
-    })
+    });
     it("displays a float with format", async () => {
-        const {getByText} = render(<Field defaultValue="titi" value={12.1} dataType="float" format="float is %.0f" /> );
+        const { getByText } = render(
+            <Field defaultValue="titi" value={12.1} dataType="float" format="float is %.0f" />
+        );
         getByText("float is 12");
-    })
+    });
+    it("displays with width=70%", async () => {
+        const { getByText } = render(<Field value="titi" width="70%" />);
+        const elt = getByText("titi");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        const { getByText } = render(<Field value="titi" width={500} />);
+        const elt = getByText("titi");
+        expect(elt).toHaveStyle("width: 500px");
+    });
 });

+ 19 - 4
frontend/taipy-gui/src/components/Taipy/Field.tsx

@@ -17,7 +17,7 @@ import Tooltip from "@mui/material/Tooltip";
 
 import { formatWSValue } from "../../utils";
 import { useClassNames, useDynamicProperty, useFormatConfig } from "../../utils/hooks";
-import { TaipyBaseProps, TaipyHoverProps } from "./utils";
+import { TaipyBaseProps, TaipyHoverProps, getCssSize } from "./utils";
 
 interface TaipyFieldProps extends TaipyBaseProps, TaipyHoverProps {
     dataType?: string;
@@ -26,6 +26,7 @@ interface TaipyFieldProps extends TaipyBaseProps, TaipyHoverProps {
     format?: string;
     raw?: boolean;
     mode?: string;
+    width?: string | number;
 }
 
 const unsetWeightSx = { fontWeight: "unset" };
@@ -41,6 +42,18 @@ const Field = (props: TaipyFieldProps) => {
 
     const mode = typeof props.mode === "string" ? props.mode.toLowerCase() : undefined;
 
+    const style = useMemo(
+        () => ({ overflow: "auto", width: props.width ? getCssSize(props.width) : undefined }),
+        [props.width]
+    );
+    const typoSx = useMemo(
+        () =>
+            props.width
+                ? { ...unsetWeightSx, overflow: "auto", width: getCssSize(props.width), display: "inline-block" }
+                : unsetWeightSx,
+        [props.width]
+    );
+
     const value = useMemo(() => {
         return formatWSValue(
             props.value !== undefined ? props.value : defaultValue || "",
@@ -53,15 +66,17 @@ const Field = (props: TaipyFieldProps) => {
     return (
         <Tooltip title={hover || ""}>
             {mode == "pre" ? (
-                <pre className={className} id={id}>{value}</pre>
+                <pre className={className} id={id} style={style}>
+                    {value}
+                </pre>
             ) : mode == "markdown" || mode == "md" ? (
                 <Markdown className={className}>{value}</Markdown>
             ) : raw || mode == "raw" ? (
-                <span className={className} id={id}>
+                <span className={className} id={id} style={style}>
                     {value}
                 </span>
             ) : (
-                <Typography className={className} id={id} component="span" sx={unsetWeightSx}>
+                <Typography className={className} id={id} component="span" sx={typoSx}>
                     {value}
                 </Typography>
             )}

+ 44 - 19
frontend/taipy-gui/src/components/Taipy/FileDownload.spec.tsx

@@ -15,7 +15,7 @@ import React from "react";
 import { render, waitFor } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
-import { newServer } from 'mock-xmlhttprequest';
+import { newServer } from "mock-xmlhttprequest";
 
 import FileDownload from "./FileDownload";
 import { TaipyContext } from "../../context/taipyContext";
@@ -33,7 +33,9 @@ describe("FileDownload Component", () => {
         expect(elt.parentElement).toHaveClass("taipy-file-download");
     });
     it("displays the default content", async () => {
-        const { getByRole } = render(<FileDownload defaultContent="/url/toto.png" content={undefined as unknown as string} />);
+        const { getByRole } = render(
+            <FileDownload defaultContent="/url/toto.png" content={undefined as unknown as string} />
+        );
         const elt = getByRole("button");
         const aElt = elt.parentElement?.querySelector("a");
         expect(aElt).toBeEmptyDOMElement();
@@ -45,6 +47,16 @@ describe("FileDownload Component", () => {
         );
         getByText("titi");
     });
+    it("displays with width=70%", async () => {
+        const { getByRole } = render(<FileDownload defaultContent="/url/toto.png" width="70%" />);
+        const elt = getByRole("button");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        const { getByRole } = render(<FileDownload defaultContent="/url/toto.png" width={500} />);
+        const elt = getByRole("button");
+        expect(elt).toHaveStyle("width: 500px");
+    });
     it("is disabled", async () => {
         const { getByRole } = render(<FileDownload defaultContent="/url/toto.png" active={false} />);
         const elt = getByRole("button");
@@ -70,35 +82,48 @@ describe("FileDownload Component", () => {
         );
         const elt = getByText("label");
         await userEvent.click(elt);
-        await waitFor(() => expect(dispatch).toHaveBeenCalledWith({
-            name: "anId",
-            payload: { args: ["from.png", ""], action: "on_action" },
-            type: "SEND_ACTION_ACTION",
-        }));
+        await waitFor(() =>
+            expect(dispatch).toHaveBeenCalledWith({
+                name: "anId",
+                payload: { args: ["from.png", ""], action: "on_action" },
+                type: "SEND_ACTION_ACTION",
+            })
+        );
     });
     it("dispatch a well formed message when content is not empty", async () => {
         const server = newServer({
-            get: ['/some/link/to.png?bypass=', {
-              // status: 200 is the default
-              //headers: { 'Content-Type': 'application/json' },
-              body: '{ "message": "Success!" }',
-            }],
-          });
+            get: [
+                "/some/link/to.png?bypass=",
+                {
+                    // status: 200 is the default
+                    //headers: { 'Content-Type': 'application/json' },
+                    body: '{ "message": "Success!" }',
+                },
+            ],
+        });
         server.install();
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
         const { getByText } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <FileDownload defaultContent="/some/link/to.png" onAction="on_action" id="anId" name="from.png" label="label" />
+                <FileDownload
+                    defaultContent="/some/link/to.png"
+                    onAction="on_action"
+                    id="anId"
+                    name="from.png"
+                    label="label"
+                />
             </TaipyContext.Provider>
         );
         const elt = getByText("label");
         await userEvent.click(elt);
-        await waitFor(() => expect(dispatch).toHaveBeenCalledWith({
-            name: "anId",
-            payload: { args: ["from.png", "/some/link/to.png?bypass="], action: "on_action" },
-            type: "SEND_ACTION_ACTION",
-        }));
+        await waitFor(() =>
+            expect(dispatch).toHaveBeenCalledWith({
+                name: "anId",
+                payload: { args: ["from.png", "/some/link/to.png?bypass="], action: "on_action" },
+                type: "SEND_ACTION_ACTION",
+            })
+        );
         server.remove();
     });
 });

+ 12 - 2
frontend/taipy-gui/src/components/Taipy/FileDownload.tsx

@@ -18,7 +18,7 @@ import Tooltip from "@mui/material/Tooltip";
 import FileDownloadIco from "@mui/icons-material/FileDownload";
 
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
-import { noDisplayStyle, TaipyActiveProps } from "./utils";
+import { getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils";
 import { createSendActionNameAction } from "../../context/taipyReducers";
 import { runXHR } from "../../utils/downloads";
 
@@ -33,6 +33,7 @@ interface FileDownloadProps extends TaipyActiveProps {
     defaultRender?: boolean;
     bypassPreview?: boolean;
     onAction?: string;
+    width?: string | number;
 }
 
 const FileDownload = (props: FileDownloadProps) => {
@@ -47,6 +48,8 @@ const FileDownload = (props: FileDownloadProps) => {
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
     const linkId = useMemo(() => (id || `tp-${Date.now()}-${Math.random()}`) + "-download-file", [id]);
 
+    const buttonSx = useMemo(() => (props.width ? { width: getCssSize(props.width) } : undefined), [props.width]);
+
     const [url, download] = useMemo(() => {
         const url = props.content || props.defaultContent || "";
         if (!url || url.startsWith("data:")) {
@@ -97,7 +100,14 @@ const FileDownload = (props: FileDownloadProps) => {
             <a style={noDisplayStyle} id={linkId} download={download} {...aProps} ref={aRef} />
             {auto ? null : (
                 <Tooltip title={hover || ""}>
-                    <Button id={id} variant="outlined" aria-label="download" disabled={!active} onClick={clickHandler}>
+                    <Button
+                        id={id}
+                        variant="outlined"
+                        aria-label="download"
+                        disabled={!active}
+                        onClick={clickHandler}
+                        sx={buttonSx}
+                    >
                         <FileDownloadIco /> {label || defaultLabel}
                     </Button>
                 </Tooltip>

+ 19 - 9
frontend/taipy-gui/src/components/Taipy/FileSelector.spec.tsx

@@ -40,6 +40,16 @@ describe("FileSelector Component", () => {
         const { getByText } = render(<FileSelector defaultLabel="titi" label={undefined as unknown as string} />);
         getByText("titi");
     });
+    it("displays with width=70%", async () => {
+        const { getByText } = render(<FileSelector label="toto" width="70%" />);
+        const elt = getByText("toto");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        const { getByText } = render(<FileSelector label="toto" width={500} />);
+        const elt = getByText("toto");
+        expect(elt).toHaveStyle("width: 500px");
+    });
     it("is disabled", async () => {
         const { getByText } = render(<FileSelector label="val" active={false} />);
         const elt = getByText("val");
@@ -64,7 +74,7 @@ describe("FileSelector Component", () => {
         const { getByText } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <FileSelector label="FileSelector" onAction="on_action" />
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
         const elt = getByText("FileSelector");
         const inputElt = elt.parentElement?.parentElement?.querySelector("input");
@@ -93,7 +103,7 @@ describe("FileSelector Component", () => {
     it("displays a dropped custom message", async () => {
         const file = new File(["(⌐□_□)"], "chucknorris2.png", { type: "image/png" });
         const { getByRole, getByText } = render(
-            <FileSelector label="FileSelectorDrop" dropMessage="drop here those files" />,
+            <FileSelector label="FileSelectorDrop" dropMessage="drop here those files" />
         );
         const elt = getByRole("button");
         const inputElt = elt.parentElement?.parentElement?.querySelector("input");
@@ -164,7 +174,7 @@ describe("FileSelector Component", () => {
         const { getByLabelText } = render(
             <TaipyContext.Provider value={{ state: INITIAL_STATE, dispatch: mockDispatch }}>
                 <FileSelector label="FileSelector" notify={true} />
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
 
         // Simulate file upload
@@ -183,7 +193,7 @@ describe("FileSelector Component", () => {
                 duration: 3000,
                 message: "mocked response",
                 system: false,
-            }),
+            })
         );
     });
 
@@ -200,7 +210,7 @@ describe("FileSelector Component", () => {
         const { getByLabelText } = render(
             <TaipyContext.Provider value={{ state: INITIAL_STATE, dispatch: mockDispatch }}>
                 <FileSelector label="FileSelector" notify={true} />
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
 
         // Simulate file upload
@@ -219,7 +229,7 @@ describe("FileSelector Component", () => {
                 duration: 3000,
                 message: "Upload failed",
                 system: false,
-            }),
+            })
         );
     });
 
@@ -231,7 +241,7 @@ describe("FileSelector Component", () => {
         const { getByLabelText, queryByRole } = render(
             <TaipyContext.Provider value={{ state: INITIAL_STATE, dispatch: mockDispatch }}>
                 <FileSelector label="FileSelector" notify={true} onAction="testAction" />
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
 
         // Simulate file upload
@@ -254,7 +264,7 @@ describe("FileSelector Component", () => {
                 type: "SEND_ACTION_ACTION",
                 name: "",
                 payload: { args: [], action: "testAction" },
-            }),
+            })
         );
     });
 
@@ -263,7 +273,7 @@ describe("FileSelector Component", () => {
         const { getByLabelText } = render(
             <TaipyContext.Provider value={{ state: INITIAL_STATE, dispatch: mockDispatch }}>
                 <FileSelector label="FileSelector" notify={true} />
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
 
         // Simulate file upload without providing a file

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

@@ -20,7 +20,7 @@ import UploadFile from "@mui/icons-material/UploadFile";
 import { TaipyContext } from "../../context/taipyContext";
 import { createAlertAction, createSendActionNameAction } from "../../context/taipyReducers";
 import { useClassNames, useDynamicProperty, useModule } from "../../utils/hooks";
-import { noDisplayStyle, TaipyActiveProps } from "./utils";
+import { getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils";
 import { uploadFile } from "../../workers/fileupload";
 
 interface FileSelectorProps extends TaipyActiveProps {
@@ -31,6 +31,7 @@ interface FileSelectorProps extends TaipyActiveProps {
     extensions?: string;
     dropMessage?: string;
     notify?: boolean;
+    width?: string | number;
 }
 
 const handleDragOver = (evt: DragEvent) => {
@@ -66,6 +67,8 @@ const FileSelector = (props: FileSelectorProps) => {
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
 
+    useEffect(() => setDropSx((sx) => (props.width ? { ...sx, width: getCssSize(props.width) } : sx)), [props.width]);
+
     const handleFiles = useCallback(
         (files: FileList | undefined | null, evt: Event | ChangeEvent) => {
             evt.stopPropagation();
@@ -102,7 +105,7 @@ const FileSelector = (props: FileSelectorProps) => {
     const handleDrop = useCallback(
         (e: DragEvent) => {
             setDropLabel("");
-            setDropSx(defaultSx);
+            setDropSx((sx) => ({ ...sx, ...defaultSx }));
             handleFiles(e.dataTransfer?.files, e);
         },
         [handleFiles]
@@ -110,7 +113,7 @@ const FileSelector = (props: FileSelectorProps) => {
 
     const handleDragLeave = useCallback(() => {
         setDropLabel("");
-        setDropSx(defaultSx);
+        setDropSx((sx) => ({ ...sx, ...defaultSx }));
     }, []);
 
     const handleDragOverWithLabel = useCallback(
@@ -118,7 +121,7 @@ const FileSelector = (props: FileSelectorProps) => {
             console.log(evt);
             const target = evt.currentTarget as HTMLElement;
             setDropSx((sx) =>
-                sx.minWidth === defaultSx.minWidth && target ? { minWidth: target.clientWidth + "px" } : sx
+                sx.minWidth === defaultSx.minWidth && target ? { ...sx, minWidth: target.clientWidth + "px" } : sx
             );
             setDropLabel(dropMessage);
             handleDragOver(evt);

+ 14 - 0
frontend/taipy-gui/src/components/Taipy/Input.spec.tsx

@@ -39,6 +39,20 @@ describe("Input Component", () => {
         );
         getByDisplayValue("titi");
     });
+    it("displays with width=70%", async () => {
+        const { getByDisplayValue, getByTestId } = render(
+            <Input value="toto" type="text" defaultValue="titi" width="70%"/>
+        );
+        const element = getByDisplayValue("toto");
+        expect(element.parentElement?.parentElement).toHaveStyle('max-width: 70%');
+    });
+    it("displays with width=500", async () => {
+        const { getByDisplayValue, getByTestId } = render(
+            <Input value="toto" type="text" defaultValue="titi" width={500}/>
+        );
+        const element = getByDisplayValue("toto");
+        expect(element.parentElement?.parentElement).toHaveStyle('max-width: 500px');
+    });
     it("is disabled", async () => {
         const { getByDisplayValue } = render(<Input value="val" type="text" active={false} />);
         const elt = getByDisplayValue("val");

+ 73 - 42
frontend/taipy-gui/src/components/Taipy/Input.tsx

@@ -21,7 +21,7 @@ import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
 import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
 
 import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers";
-import { TaipyInputProps } from "./utils";
+import { getCssSize, TaipyInputProps } from "./utils";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
 
 const AUTHORIZED_KEYS = ["Enter", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"];
@@ -80,25 +80,47 @@ const Input = (props: TaipyInputProps) => {
     const min = useDynamicProperty(props.min, props.defaultMin, undefined);
     const max = useDynamicProperty(props.max, props.defaultMax, undefined);
 
-    const handleInput = useCallback(
-        (e: React.ChangeEvent<HTMLInputElement>) => {
-            const val = e.target.value;
-            setValue(val);
+    const textSx = useMemo(
+        () =>
+            props.width
+                ? {
+                      ...numberSx,
+                      maxWidth: getCssSize(props.width),
+                  }
+                : numberSx,
+        [props.width]
+    );
+
+    const updateValueWithDelay = useCallback(
+        (value: number | string) => {
+            if (changeDelay === -1) {
+                return;
+            }
             if (changeDelay === 0) {
-                dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
+                // Workaround using microtask to ensure the value is updated before the next action to avoid the bad setState behavior
+                Promise.resolve().then(() => {
+                    dispatch(createSendUpdateAction(updateVarName, value, module, onChange, propagate));
+                });
                 return;
             }
-            if (changeDelay > 0) {
-                if (delayCall.current > 0) {
-                    clearTimeout(delayCall.current);
-                }
-                delayCall.current = window.setTimeout(() => {
-                    delayCall.current = -1;
-                    dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
-                }, changeDelay);
+            if (delayCall.current > 0) {
+                clearTimeout(delayCall.current);
             }
+            delayCall.current = window.setTimeout(() => {
+                delayCall.current = -1;
+                dispatch(createSendUpdateAction(updateVarName, value, module, onChange, propagate));
+            }, changeDelay);
+        },
+        [changeDelay, dispatch, updateVarName, module, onChange, propagate]
+    );
+
+    const handleInput = useCallback(
+        (e: React.ChangeEvent<HTMLInputElement>) => {
+            const val = e.target.value;
+            setValue(val);
+            dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
         },
-        [updateVarName, dispatch, propagate, onChange, changeDelay, module]
+        [updateVarName, dispatch, propagate, onChange, module]
     );
 
     const handleAction = useCallback(
@@ -112,6 +134,7 @@ const Input = (props: TaipyInputProps) => {
                         val = max;
                     }
                     setValue(val.toString());
+                    updateValueWithDelay(val);
                     evt.preventDefault();
                 } else if (evt.key === "ArrowDown") {
                     let val =
@@ -121,6 +144,7 @@ const Input = (props: TaipyInputProps) => {
                         val = min;
                     }
                     setValue(val.toString());
+                    updateValueWithDelay(val);
                     evt.preventDefault();
                 }
             } else if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
@@ -142,13 +166,14 @@ const Input = (props: TaipyInputProps) => {
             step,
             stepMultiplier,
             max,
-            min,
-            changeDelay,
+            updateValueWithDelay,
             onAction,
             dispatch,
             id,
             module,
             updateVarName,
+            min,
+            changeDelay,
             onChange,
             propagate,
         ]
@@ -179,16 +204,22 @@ const Input = (props: TaipyInputProps) => {
                     event.shiftKey,
                     increment
                 );
+
                 if (min !== undefined && Number(newValue) < min) {
+                    updateValueWithDelay(min);
                     return min.toString();
                 }
+
                 if (max !== undefined && Number(newValue) > max) {
+                    updateValueWithDelay(max);
                     return max.toString();
                 }
+
+                updateValueWithDelay(newValue);
                 return newValue;
             });
         },
-        [min, max, step, stepMultiplier, calculateNewValue]
+        [calculateNewValue, step, stepMultiplier, min, max, updateValueWithDelay]
     );
 
     const handleUpStepperMouseDown = useCallback(
@@ -228,27 +259,27 @@ const Input = (props: TaipyInputProps) => {
                       ),
                   }
                 : type == "number"
-                ? {
-                      endAdornment: (
-                          <div style={verticalDivStyle}>
-                              <IconButton
-                                  aria-label="Increment value"
-                                  size="small"
-                                  onMouseDown={handleUpStepperMouseDown}
-                              >
-                                  <ArrowDropUpIcon fontSize="inherit" />
-                              </IconButton>
-                              <IconButton
-                                  aria-label="Decrement value"
-                                  size="small"
-                                  onMouseDown={handleDownStepperMouseDown}
-                              >
-                                  <ArrowDropDownIcon fontSize="inherit" />
-                              </IconButton>
-                          </div>
-                      ),
-                  }
-                : undefined,
+                  ? {
+                        endAdornment: (
+                            <div style={verticalDivStyle}>
+                                <IconButton
+                                    aria-label="Increment value"
+                                    size="small"
+                                    onMouseDown={handleUpStepperMouseDown}
+                                >
+                                    <ArrowDropUpIcon fontSize="inherit" />
+                                </IconButton>
+                                <IconButton
+                                    aria-label="Decrement value"
+                                    size="small"
+                                    onMouseDown={handleDownStepperMouseDown}
+                                >
+                                    <ArrowDropDownIcon fontSize="inherit" />
+                                </IconButton>
+                            </div>
+                        ),
+                    }
+                  : undefined,
         [
             type,
             showPassword,
@@ -268,8 +299,8 @@ const Input = (props: TaipyInputProps) => {
                       max: max,
                   }
                 : type == "password"
-                ? { autoComplete: "current-password" }
-                : undefined,
+                  ? { autoComplete: "current-password" }
+                  : undefined,
         [type, step, min, max]
     );
 
@@ -282,7 +313,7 @@ const Input = (props: TaipyInputProps) => {
     return (
         <Tooltip title={hover || ""}>
             <TextField
-                sx={numberSx}
+                sx={textSx}
                 margin="dense"
                 hiddenLabel
                 value={value ?? ""}

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

@@ -173,10 +173,6 @@ const styledColumns = JSON.stringify({
     },
 });
 
-const invalidColumns = JSON.stringify({
-    invalid: true,
-});
-
 describe("PaginatedTable Component", () => {
     it("renders", async () => {
         const { getByText } = render(<PaginatedTable data={undefined} defaultColumns={tableColumns} />);
@@ -655,7 +651,7 @@ describe("PaginatedTable Component", () => {
                 col: "int",
                 index: 1,
                 reason: "click",
-                value: undefined
+                value: undefined,
             },
             type: "SEND_ACTION_ACTION",
         });
@@ -692,7 +688,7 @@ describe("PaginatedTable Component", () => {
                 col: "Code",
                 index: 0,
                 reason: "button",
-                value: "button action"
+                value: "button action",
             },
             type: "SEND_ACTION_ACTION",
         });
@@ -714,14 +710,6 @@ describe("PaginatedTable Component", () => {
         const elt = document.querySelector('table[aria-labelledby="tableTitle"]');
         expect(elt).toBeInTheDocument();
     });
-    it("logs error when baseColumns prop is invalid", () => {
-        // Mock console.info to check if it gets called
-        console.info = jest.fn();
-        // Render the component with invalid baseColumns prop
-        render(<PaginatedTable defaultColumns={invalidColumns} />);
-        // Check if console.info was called
-        expect(console.info).toHaveBeenCalled();
-    });
     it("should sort the table in ascending order", async () => {
         await waitFor(() => {
             render(<PaginatedTable data={tableValue} defaultColumns={tableColumns} />);

+ 25 - 22
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -136,34 +136,36 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         if (baseColumns) {
             try {
                 let filter = false;
-                Object.values(baseColumns).forEach((col) => {
-                    if (typeof col.filter != "boolean") {
-                        col.filter = !!props.filter;
+                const newCols: Record<string, ColumnDesc> = {};
+                Object.entries(baseColumns).forEach(([cId, cDesc]) => {
+                    const nDesc = (newCols[cId] = { ...cDesc });
+                    if (typeof nDesc.filter != "boolean") {
+                        nDesc.filter = !!props.filter;
                     }
-                    filter = filter || col.filter;
-                    if (typeof col.notEditable != "boolean") {
-                        col.notEditable = !editable;
+                    filter = filter || nDesc.filter;
+                    if (typeof nDesc.notEditable != "boolean") {
+                        nDesc.notEditable = !editable;
                     }
-                    if (col.tooltip === undefined) {
-                        col.tooltip = props.tooltip;
+                    if (nDesc.tooltip === undefined) {
+                        nDesc.tooltip = props.tooltip;
                     }
                 });
                 addDeleteColumn(
                     (active && editable && (onAdd || onDelete) ? 1 : 0) +
                         (active && filter ? 1 : 0) +
                         (active && downloadable ? 1 : 0),
-                    baseColumns
+                    newCols
                 );
-                const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
+                const colsOrder = Object.keys(newCols).sort(getsortByIndex(newCols));
                 const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
-                    if (baseColumns[col].style) {
+                    if (newCols[col].style) {
                         pv.styles = pv.styles || {};
-                        pv.styles[baseColumns[col].dfid] = baseColumns[col].style as string;
+                        pv.styles[newCols[col].dfid] = newCols[col].style as string;
                     }
-                    hNan = hNan || !!baseColumns[col].nanValue;
-                    if (baseColumns[col].tooltip) {
+                    hNan = hNan || !!newCols[col].nanValue;
+                    if (newCols[col].tooltip) {
                         pv.tooltips = pv.tooltips || {};
-                        pv.tooltips[baseColumns[col].dfid] = baseColumns[col].tooltip as string;
+                        pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
                     }
                     return pv;
                 }, {});
@@ -171,7 +173,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                     styTt.styles = styTt.styles || {};
                     styTt.styles[LINE_STYLE] = props.lineStyle;
                 }
-                return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
+                return [colsOrder, newCols, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
                 console.info("PaginatedTable.columns: ", (e as Error).message || e);
             }
@@ -293,7 +295,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         module,
         compare,
         onCompare,
-        userData
+        userData,
     ]);
 
     const onSort = useCallback(
@@ -480,7 +482,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [
-                                                    active && onAdd ? (
+                                                    active && editable && onAdd ? (
                                                         <Tooltip title="Add a row" key="addARow">
                                                             <IconButton
                                                                 onClick={onAddRowClick}
@@ -561,11 +563,10 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                 {rows.map((row, index) => {
                                     const sel = selected.indexOf(index + startIndex);
                                     if (sel == 0) {
-                                        setTimeout(
+                                        Promise.resolve().then(
                                             () =>
                                                 selectedRowRef.current?.scrollIntoView &&
-                                                selectedRowRef.current.scrollIntoView({ block: "center" }),
-                                            1
+                                                selectedRowRef.current.scrollIntoView({ block: "center" })
                                         );
                                     }
                                     return (
@@ -592,7 +593,9 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                                             ? onCellValidation
                                                             : undefined
                                                     }
-                                                    onDeletion={active && onDelete ? onRowDeletion : undefined}
+                                                    onDeletion={
+                                                        active && editable && onDelete ? onRowDeletion : undefined
+                                                    }
                                                     onSelection={active && onAction ? onRowSelection : undefined}
                                                     nanValue={columns[col].nanValue || props.nanValue}
                                                     tooltip={getTooltip(row, columns[col].tooltip, col)}

+ 69 - 31
frontend/taipy-gui/src/components/Taipy/ThemeToggle.spec.tsx

@@ -12,11 +12,11 @@
  */
 
 import React from "react";
-import {render} from "@testing-library/react";
+import { render } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
-import ThemeToggle from './ThemeToggle';
+import ThemeToggle from "./ThemeToggle";
 import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers";
 import { TaipyContext } from "../../context/taipyContext";
 
@@ -31,65 +31,103 @@ beforeEach(() => {
 
 describe("ThemeToggle Component", () => {
     it("renders", async () => {
-        const { getByText, getByTestId, getByTitle } = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle />
-        </TaipyContext.Provider>);
+        const { getByText, getByTestId, getByTitle } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle />
+            </TaipyContext.Provider>
+        );
         expect(getByTestId("Brightness3Icon")).toBeInTheDocument();
         expect(getByTestId("WbSunnyIcon")).toBeInTheDocument();
         expect(getByTitle("Light")).toBeInTheDocument();
         expect(getByTitle("Dark")).toBeInTheDocument();
         const label = getByText("Mode");
         expect(label.tagName).toBe("P");
-    })
+    });
     it("uses the class", async () => {
-        const {getByText} = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle className="taipy-toggle" />
-        </TaipyContext.Provider>);
+        const { getByText } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle className="taipy-toggle" />
+            </TaipyContext.Provider>
+        );
         const elt = getByText("Mode");
         expect(elt.parentElement).toHaveClass("taipy-toggle");
-    })
+    });
     it("shows Light theme selected at start", async () => {
-        const {getByTitle} = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle />
-        </TaipyContext.Provider>);
+        const { getByTitle } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle />
+            </TaipyContext.Provider>
+        );
         expect(getByTitle("Dark")).not.toHaveClass("Mui-selected");
         expect(getByTitle("Light")).toHaveClass("Mui-selected");
     });
     it("shows Dark theme selected at start", async () => {
         state.theme.palette.mode = "dark";
-        const {getByTitle} = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle />
-        </TaipyContext.Provider>);
+        const { getByTitle } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle />
+            </TaipyContext.Provider>
+        );
         expect(getByTitle("Dark")).toHaveClass("Mui-selected");
         expect(getByTitle("Light")).not.toHaveClass("Mui-selected");
     });
+    it("displays with width=70%", async () => {
+        render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle width="70%" />
+            </TaipyContext.Provider>
+        );
+        const elt = document.querySelector(".MuiBox-root");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle width={500} />
+            </TaipyContext.Provider>
+        );
+        const elt = document.querySelector(".MuiBox-root");
+        expect(elt).toHaveStyle("width: 500px");
+    });
     it("is disabled", async () => {
-        const { getAllByRole } = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle active={false} />
-        </TaipyContext.Provider>);
+        const { getAllByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle active={false} />
+            </TaipyContext.Provider>
+        );
         const elts = getAllByRole("button");
-        elts.forEach(elt => expect(elt).toBeDisabled());
+        elts.forEach((elt) => expect(elt).toBeDisabled());
     });
     it("is enabled by default", async () => {
-        const { getAllByRole } = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle />
-        </TaipyContext.Provider>);
+        const { getAllByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle />
+            </TaipyContext.Provider>
+        );
         const elts = getAllByRole("button");
-        elts.forEach(elt => expect(elt).not.toBeDisabled());
+        elts.forEach((elt) => expect(elt).not.toBeDisabled());
     });
     it("is enabled by active", async () => {
-        const { getAllByRole } = render(<TaipyContext.Provider value={{ state, dispatch }}>
-            <ThemeToggle active={true}/>
-        </TaipyContext.Provider>);
+        const { getAllByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <ThemeToggle active={true} />
+            </TaipyContext.Provider>
+        );
         const elts = getAllByRole("button");
-        elts.forEach(elt => expect(elt).not.toBeDisabled());
+        elts.forEach((elt) => expect(elt).not.toBeDisabled());
     });
     it("dispatch a well formed message", async () => {
-        const { getByTitle } = render(<TaipyContext.Provider value={{ state, dispatch }}>
+        const { getByTitle } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
                 <ThemeToggle />
-            </TaipyContext.Provider>);
+            </TaipyContext.Provider>
+        );
         const elt = getByTitle("Dark");
         await userEvent.click(elt);
-        expect(dispatch).toHaveBeenCalledWith({name: "theme", payload: {value: "dark", "fromBackend": false}, "type": "SET_THEME"});
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "theme",
+            payload: { value: "dark", fromBackend: false },
+            type: "SET_THEME",
+        });
     });
 });

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

@@ -11,24 +11,25 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { CSSProperties, MouseEvent, useCallback, useContext, useEffect, useMemo } from "react";
+import React, { MouseEvent, useCallback, useContext, useEffect, useMemo } from "react";
 import Box from "@mui/material/Box";
 import Typography from "@mui/material/Typography";
-import { PaletteMode } from "@mui/material";
+import { PaletteMode, SxProps } from "@mui/material";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 import WbSunny from "@mui/icons-material/WbSunny";
 import Brightness3 from "@mui/icons-material/Brightness3";
 
-import { TaipyActiveProps, emptyStyle } from "./utils";
+import { TaipyActiveProps, getCssSize } from "./utils";
 import { TaipyContext } from "../../context/taipyContext";
 import { createThemeAction } from "../../context/taipyReducers";
 import { useClassNames } from "../../utils/hooks";
 import { getLocalStorageValue } from "../../context/utils";
 
 interface ThemeToggleProps extends TaipyActiveProps {
-    style?: CSSProperties;
+    style?: SxProps;
     label?: string;
+    width?: string | number;
 }
 
 const boxSx = {
@@ -41,7 +42,9 @@ const boxSx = {
     "& > *": {
         m: 1,
     },
-} as CSSProperties;
+} as SxProps;
+
+export const emptyStyle = {} as SxProps;
 
 const groupSx = { verticalAlign: "middle" };
 
@@ -63,7 +66,14 @@ const ThemeToggle = (props: ThemeToggleProps) => {
         }
     }, [state.theme.palette.mode, dispatch]);
 
-    const mainSx = useMemo(() => ({ ...boxSx, ...style }), [style]);
+    const mainSx = useMemo(
+        () =>
+            props.width
+                ? ({ ...boxSx, ...style, width: getCssSize(props.width) } as SxProps)
+                : ({ ...boxSx, ...style } as SxProps),
+        [style, props.width]
+    );
+
     return (
         <Box id={id} sx={mainSx} className={className}>
             <Typography>{label}</Typography>
@@ -74,6 +84,7 @@ const ThemeToggle = (props: ThemeToggleProps) => {
                 aria-label="Theme mode"
                 disabled={!active}
                 sx={groupSx}
+                fullWidth={!!props.width}
             >
                 <ToggleButton value="light" aria-label="light" title="Light">
                     <WbSunny />

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

@@ -82,6 +82,16 @@ describe("Toggle Component", () => {
         const elt2 = getByText("Item 2");
         expect(elt2.parentElement).toHaveClass("Mui-selected");
     });
+    it("displays with width=70%", async () => {
+        render(<Toggle lov={lov} width="70%" />);
+        const elt = document.querySelector(".MuiBox-root");
+        expect(elt).toHaveStyle("width: 70%");
+    });
+    it("displays with width=500", async () => {
+        render(<Toggle lov={lov} width={500} />);
+        const elt = document.querySelector(".MuiBox-root");
+        expect(elt).toHaveStyle("width: 500px");
+    });
     it("is disabled", async () => {
         const { getAllByRole } = render(<Toggle lov={lov} active={false} />);
         const elts = getAllByRole("button");
@@ -151,32 +161,47 @@ describe("Toggle Component", () => {
             expect(elt.tagName).toBe("SPAN");
         });
         it("uses the class", async () => {
-            const { getByText } = render(<Toggle isSwitch={true}  label="switch" className="taipy-toggle" />);
+            const { getByText } = render(<Toggle isSwitch={true} label="switch" className="taipy-toggle" />);
             const elt = getByText("switch");
             expect(elt.parentElement).toHaveClass("taipy-toggle-switch");
         });
         it("shows a selection at start", async () => {
-            const { getByText } = render(<Toggle isSwitch={true} defaultValue={true as unknown as string} label="switch" />);
+            const { getByText } = render(
+                <Toggle isSwitch={true} defaultValue={true as unknown as string} label="switch" />
+            );
             const elt = getByText("switch");
             expect(elt.parentElement?.querySelector(".MuiSwitch-switchBase")).toHaveClass("Mui-checked");
         });
         it("shows a selection at start through value", async () => {
-            const { getByText } = render(<Toggle isSwitch={true} value={true as unknown as string} defaultValue={false as unknown as string} label="switch" />);
+            const { getByText } = render(
+                <Toggle
+                    isSwitch={true}
+                    value={true as unknown as string}
+                    defaultValue={false as unknown as string}
+                    label="switch"
+                />
+            );
             const elt = getByText("switch");
             expect(elt.parentElement?.querySelector(".MuiSwitch-switchBase")).toHaveClass("Mui-checked");
         });
         it("is disabled", async () => {
-            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={false} />);
+            const { getByText } = render(
+                <Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={false} />
+            );
             const elt = getByText("switch");
             expect(elt.parentElement?.querySelector("input")).toBeDisabled();
         });
         it("is enabled by default", async () => {
-            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" />);
+            const { getByText } = render(
+                <Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" />
+            );
             const elt = getByText("switch");
             expect(elt.parentElement?.querySelector("input")).not.toBeDisabled();
         });
         it("is enabled by active", async () => {
-            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={true} />);
+            const { getByText } = render(
+                <Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={true} />
+            );
             const elt = getByText("switch");
             expect(elt.parentElement?.querySelector("input")).not.toBeDisabled();
         });
@@ -185,7 +210,12 @@ describe("Toggle Component", () => {
             const state: TaipyState = INITIAL_STATE;
             const { getByText } = render(
                 <TaipyContext.Provider value={{ state, dispatch }}>
-                    <Toggle isSwitch={true} updateVarName="varname" defaultValue={false as unknown as string} label="switch" />
+                    <Toggle
+                        isSwitch={true}
+                        updateVarName="varname"
+                        defaultValue={false as unknown as string}
+                        label="switch"
+                    />
                 </TaipyContext.Provider>
             );
             const elt = getByText("switch");
@@ -197,6 +227,5 @@ describe("Toggle Component", () => {
                 type: "SEND_UPDATE_ACTION",
             });
         });
-
     });
 });

+ 22 - 9
frontend/taipy-gui/src/components/Taipy/Toggle.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { CSSProperties, MouseEvent, SyntheticEvent, useCallback, useEffect, useState } from "react";
+import React, { MouseEvent, SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react";
 import Box from "@mui/material/Box";
 import Switch from "@mui/material/Switch";
 import Typography from "@mui/material/Typography";
@@ -20,22 +20,23 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 import Tooltip from "@mui/material/Tooltip";
 
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import ThemeToggle from "./ThemeToggle";
+import ThemeToggle, { emptyStyle } from "./ThemeToggle";
 import { LovProps, useLovListMemo } from "./lovUtils";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
-import { emptyStyle, getSuffixedClassNames, getUpdateVar } from "./utils";
+import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 import { Icon, IconAvatar } from "../../utils/icon";
-import { FormControlLabel } from "@mui/material";
+import { FormControlLabel, SxProps } from "@mui/material";
 
-const groupSx = { verticalAlign: "middle" };
+const baseGroupSx = { verticalAlign: "middle" };
 
 interface ToggleProps extends LovProps<string> {
-    style?: CSSProperties;
+    style?: SxProps;
     label?: string;
     unselectedValue?: string;
     allowUnselect?: boolean;
     mode?: string;
-    isSwitch? : boolean;
+    isSwitch?: boolean;
+    width?: string | number;
 }
 
 const Toggle = (props: ToggleProps) => {
@@ -70,6 +71,11 @@ const Toggle = (props: ToggleProps) => {
 
     const lovList = useLovListMemo(lov, defaultLov);
 
+    const boxSx = useMemo(
+        () => (props.width ? ({ ...style, width: getCssSize(props.width) } as SxProps) : style),
+        [props.width, style]
+    );
+
     const changeValue = useCallback(
         (evt: MouseEvent, val: string) => {
             if (!props.allowUnselect && val === null) {
@@ -112,7 +118,7 @@ const Toggle = (props: ToggleProps) => {
     return mode.toLowerCase() === "theme" ? (
         <ThemeToggle {...props} />
     ) : (
-        <Box id={id} sx={style} className={className}>
+        <Box id={id} sx={boxSx} className={className}>
             {label && !isSwitch ? <Typography>{label}</Typography> : null}
             <Tooltip title={hover || ""}>
                 {isSwitch ? (
@@ -125,7 +131,14 @@ const Toggle = (props: ToggleProps) => {
                         className={getSuffixedClassNames(className, "-switch")}
                     />
                 ) : (
-                    <ToggleButtonGroup value={value} exclusive onChange={changeValue} disabled={!active} sx={groupSx}>
+                    <ToggleButtonGroup
+                        value={value}
+                        exclusive
+                        onChange={changeValue}
+                        disabled={!active}
+                        sx={baseGroupSx}
+                        fullWidth={!!props.width}
+                    >
                         {lovList &&
                             lovList.map((v) => (
                                 <ToggleButton value={v.id} key={v.id}>

+ 5 - 6
frontend/taipy-gui/src/components/Taipy/utils.ts

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import { CSSProperties, MouseEvent } from "react";
+import { MouseEvent } from "react";
 
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
     defaultActive?: boolean;
@@ -61,6 +61,7 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai
     actionKeys?: string;
     multiline?: boolean;
     linesShown?: number;
+    width?: string | number;
 }
 
 export interface TaipyLabelProps {
@@ -127,8 +128,6 @@ export const getSuffixedClassNames = (names: string | undefined, suffix: string)
         .map((n) => n + suffix)
         .join(" ");
 
-export const emptyStyle = {} as CSSProperties;
-
 export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
 
 export const getProps = (p: DateProps, start: boolean, val: Date | null, withTime: boolean): DateProps => {
@@ -140,10 +139,10 @@ export const getProps = (p: DateProps, start: boolean, val: Date | null, withTim
             ? "minDateTime"
             : "maxDateTime"
         : start
-            ? "minDate"
-            : "maxDate";
+        ? "minDate"
+        : "maxDate";
     if (p[propName] == val) {
         return p;
     }
-    return {...p, [propName]: val};
+    return { ...p, [propName]: val };
 };

+ 4 - 2
frontend/taipy-gui/src/utils/ErrorBoundary.tsx

@@ -23,8 +23,10 @@ interface ErrorFallBackProps {
 const ErrorFallback = (props: ErrorFallBackProps) => (
     <Box sx={{ backgroundColor: "error.main" }}>
         <Box>Something went wrong ...</Box>
-        <Box>{(props.error as Error).message}</Box>
-        <Button onClick={props.resetErrorBoundary}>Try again</Button>
+        <Box>{props.error.message}</Box>
+        <Button onClick={props.resetErrorBoundary} color="secondary">
+            Try again
+        </Button>
     </Box>
 );
 

+ 80 - 91
frontend/taipy/package-lock.json

@@ -56,11 +56,11 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
-      "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
+      "version": "7.25.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz",
+      "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==",
       "dependencies": {
-        "@babel/types": "^7.25.0",
+        "@babel/types": "^7.25.4",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -112,11 +112,11 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
-      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz",
+      "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==",
       "dependencies": {
-        "@babel/types": "^7.25.2"
+        "@babel/types": "^7.25.4"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -126,9 +126,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
-      "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz",
+      "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -150,15 +150,15 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
-      "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz",
+      "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.25.0",
-        "@babel/parser": "^7.25.3",
+        "@babel/generator": "^7.25.4",
+        "@babel/parser": "^7.25.4",
         "@babel/template": "^7.25.0",
-        "@babel/types": "^7.25.2",
+        "@babel/types": "^7.25.4",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -167,9 +167,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
-      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "version": "7.25.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz",
+      "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==",
       "dependencies": {
         "@babel/helper-string-parser": "^7.24.8",
         "@babel/helper-validator-identifier": "^7.24.7",
@@ -237,14 +237,14 @@
       "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
     },
     "node_modules/@emotion/react": {
-      "version": "11.13.0",
-      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz",
-      "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==",
+      "version": "11.13.3",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
+      "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
       "dependencies": {
         "@babel/runtime": "^7.18.3",
         "@emotion/babel-plugin": "^11.12.0",
         "@emotion/cache": "^11.13.0",
-        "@emotion/serialize": "^1.3.0",
+        "@emotion/serialize": "^1.3.1",
         "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
         "@emotion/utils": "^1.4.0",
         "@emotion/weak-memoize": "^0.4.0",
@@ -260,13 +260,13 @@
       }
     },
     "node_modules/@emotion/serialize": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz",
-      "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz",
+      "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==",
       "dependencies": {
         "@emotion/hash": "^0.9.2",
         "@emotion/memoize": "^0.9.0",
-        "@emotion/unitless": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
         "@emotion/utils": "^1.4.0",
         "csstype": "^3.0.2"
       }
@@ -299,9 +299,9 @@
       }
     },
     "node_modules/@emotion/unitless": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz",
-      "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ=="
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
     },
     "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
       "version": "1.1.0",
@@ -880,14 +880,14 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.12.1.tgz",
-      "integrity": "sha512-Zj8kt3SCQbJp1qhMi+A3I4KqB8i5OY2Q11mdOEathFhqN/SQm1sUjIa1G09cGP1dPDgK1a6KM6qJGNtcw/nuWA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.14.0.tgz",
+      "integrity": "sha512-3xI3xYVxqPU4//KfE4FcR+Zs7UT4kkDPvA+IDOcQdRUyVwmcXCjBuJZgKgJMqSCNK/KIJZQQrpmy5XGHOKTbdA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -944,12 +944,12 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.12.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.12.0.tgz",
-      "integrity": "sha512-zgu/JqSXBflSvtzfFN8lNi5Wxw79czBv6V/crOrXqCCOzxAIsrcup2FZlwvXlzetm3otS7o/Tzfo/O5dE68NkA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.14.0.tgz",
+      "integrity": "sha512-+qWIHLgt2vgH6bKmf7IwRvS86UbZRWKAdDY/yTQJaqzCzyesUvQhD+WRxe1kpdCK8UE061S9/Ju7hLkM4kjRNA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/utils": "^5.16.5"
+        "@mui/utils": "^5.16.6"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -963,15 +963,15 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.12.1",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.12.1.tgz",
-      "integrity": "sha512-WEejS6mzKQzwm0vKT5W1XqlHxqIFv0AV/MYDgvru39WwaCUCyip32sjvl7cDNwrsC8CkwyBCaEvNDEE9Jx0BkA==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.14.0.tgz",
+      "integrity": "sha512-j1sK0tLrsiCu0FxwTJQkVm2nbLYc1tRLwmPDAXcQ3nuzGDzn0x/IA28dBjxse/+oNy4j2cpJz3k/mSz/a4ZLjA==",
       "dependencies": {
         "@babel/runtime": "^7.25.0",
-        "@mui/system": "^5.16.5",
-        "@mui/utils": "^5.16.5",
-        "@mui/x-internals": "7.12.0",
-        "@types/react-transition-group": "^4.4.10",
+        "@mui/system": "^5.16.7",
+        "@mui/utils": "^5.16.6",
+        "@mui/x-internals": "7.14.0",
+        "@types/react-transition-group": "^4.4.11",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "react-transition-group": "^4.4.5"
@@ -1124,25 +1124,15 @@
       "dev": true
     },
     "node_modules/@types/eslint": {
-      "version": "8.56.11",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz",
-      "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==",
+      "version": "8.56.12",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+      "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
       "dev": true,
       "dependencies": {
         "@types/estree": "*",
         "@types/json-schema": "*"
       }
     },
-    "node_modules/@types/eslint-scope": {
-      "version": "3.7.7",
-      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
-      "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
-      "dev": true,
-      "dependencies": {
-        "@types/eslint": "*",
-        "@types/estree": "*"
-      }
-    },
     "node_modules/@types/estree": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@@ -1189,12 +1179,12 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "22.3.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz",
-      "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==",
+      "version": "22.5.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
+      "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
       "dev": true,
       "dependencies": {
-        "undici-types": "~6.18.2"
+        "undici-types": "~6.19.2"
       }
     },
     "node_modules/@types/parse-json": {
@@ -1208,9 +1198,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     "node_modules/@types/react": {
-      "version": "18.3.3",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
-      "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
+      "version": "18.3.4",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
+      "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
       "dependencies": {
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
@@ -2013,9 +2003,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001651",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
-      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "version": "1.0.30001653",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
+      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
       "dev": true,
       "funding": [
         {
@@ -2354,9 +2344,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.7",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.7.tgz",
-      "integrity": "sha512-6FTNWIWMxMy/ZY6799nBlPtF1DFDQ6VQJ7yyDP27SJNt5lwtQ5ufqVvHylb3fdQefvRcgA3fKcFMJi9OLwBRNw==",
+      "version": "1.5.13",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
+      "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
       "dev": true
     },
     "node_modules/enhanced-resolve": {
@@ -3628,9 +3618,9 @@
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.15.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
-      "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+      "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
       "dependencies": {
         "hasown": "^2.0.2"
       },
@@ -4251,9 +4241,9 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "dependencies": {
         "braces": "^3.0.3",
@@ -5596,9 +5586,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
     },
     "node_modules/type-check": {
       "version": "0.4.0",
@@ -5726,9 +5716,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "6.18.2",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz",
-      "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==",
+      "version": "6.19.8",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
       "dev": true
     },
     "node_modules/update-browserslist-db": {
@@ -5784,12 +5774,11 @@
       }
     },
     "node_modules/webpack": {
-      "version": "5.93.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz",
-      "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==",
+      "version": "5.94.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
+      "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
       "dev": true,
       "dependencies": {
-        "@types/eslint-scope": "^3.7.3",
         "@types/estree": "^1.0.5",
         "@webassemblyjs/ast": "^1.12.1",
         "@webassemblyjs/wasm-edit": "^1.12.1",
@@ -5798,7 +5787,7 @@
         "acorn-import-attributes": "^1.9.5",
         "browserslist": "^4.21.10",
         "chrome-trace-event": "^1.0.2",
-        "enhanced-resolve": "^5.17.0",
+        "enhanced-resolve": "^5.17.1",
         "es-module-lexer": "^1.2.1",
         "eslint-scope": "5.1.1",
         "events": "^3.2.0",

+ 35 - 30
frontend/taipy/src/CoreSelector.tsx

@@ -378,10 +378,9 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 if (isSelectable) {
                     const lovVar = getUpdateVar(updateVars, lovPropertyName);
                     const val = nodeId;
-                    setTimeout(
+                    Promise.resolve().then(
                         // to avoid set state while render react errors
-                        () => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, lovVar)),
-                        1
+                        () => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, lovVar))
                     );
                     onSelect && onSelect(val);
                 }
@@ -421,19 +420,17 @@ const CoreSelector = (props: CoreSelectorProps) => {
             setSelectedItems((old) => {
                 if (old.length) {
                     const lovVar = getUpdateVar(updateVars, lovPropertyName);
-                    setTimeout(
-                        () =>
-                            dispatch(
-                                createSendUpdateAction(
-                                    updateVarName,
-                                    multiple ? [] : "",
-                                    module,
-                                    onChange,
-                                    propagate,
-                                    lovVar
-                                )
-                            ),
-                        1
+                    Promise.resolve().then(() =>
+                        dispatch(
+                            createSendUpdateAction(
+                                updateVarName,
+                                multiple ? [] : "",
+                                module,
+                                onChange,
+                                propagate,
+                                lovVar
+                            )
+                        )
                     );
                     return [];
                 }
@@ -511,10 +508,20 @@ const CoreSelector = (props: CoreSelectorProps) => {
     // filters
     const colFilters = useMemo(() => {
         try {
-            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string, string[]]>) : undefined;
+            const res = props.filter
+                ? (JSON.parse(props.filter) as Array<[string, string, string, string[]]>)
+                : undefined;
             return Array.isArray(res)
                 ? res.reduce((pv, [name, id, coltype, lov], idx) => {
-                      pv[name] = { dfid: id, title: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
+                      pv[name] = {
+                          dfid: id,
+                          title: name,
+                          type: coltype,
+                          index: idx,
+                          filter: true,
+                          lov: lov,
+                          freeLov: !!lov,
+                      };
                       return pv;
                   }, {} as Record<string, ColumnDesc>)
                 : undefined;
@@ -532,18 +539,16 @@ const CoreSelector = (props: CoreSelectorProps) => {
                     localStoreSet(jsonFilters, id, lovPropertyName, "filter");
                     const filterVar = getUpdateVar(updateCoreVars, "filter");
                     const lovVar = getUpdateVarNames(updateVars, lovPropertyName);
-                    setTimeout(
-                        () =>
-                            dispatch(
-                                createRequestUpdateAction(
-                                    id,
-                                    module,
-                                    lovVar,
-                                    true,
-                                    filterVar ? { [filterVar]: filters } : undefined
-                                )
-                            ),
-                        1
+                    Promise.resolve().then(() =>
+                        dispatch(
+                            createRequestUpdateAction(
+                                id,
+                                module,
+                                lovVar,
+                                true,
+                                filterVar ? { [filterVar]: filters } : undefined
+                            )
+                        )
                     );
                     return filters;
                 }

+ 2 - 1
frontend/taipy/src/DataNodeTable.tsx

@@ -215,7 +215,8 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                 updateVarName={props.updateVarName}
                 data={props.data}
                 userData={userData}
-                onEdit={tableEdit ? props.onEdit : undefined}
+                onEdit={props.onEdit}
+                editable={tableEdit}
                 filter={true}
                 libClassName="taipy-table"
                 pageSize={25}

+ 20 - 46
frontend/taipy/src/DataNodeViewer.tsx

@@ -348,16 +348,14 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
             // clean lock on change
             if (oldDn[DataNodeFullProps.id] && isNewDn && editLock.current) {
                 const oldId = oldDn[DataNodeFullProps.id];
-                setTimeout(
-                    () =>
-                        dispatch(
-                            createSendActionNameAction(id, module, props.onLock, {
-                                id: oldId,
-                                lock: false,
-                                error_id: getUpdateVar(updateDnVars, "error_id"),
-                            })
-                        ),
-                    1
+                Promise.resolve().then(() =>
+                    dispatch(
+                        createSendActionNameAction(id, module, props.onLock, {
+                            id: oldId,
+                            lock: false,
+                            error_id: getUpdateVar(updateDnVars, "error_id"),
+                        })
+                    )
                 );
             }
             if (!dn || isNewDn) {
@@ -371,18 +369,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                 if (req && !isNewDn && tabValue == TabValues.History) {
                     const idVar = getUpdateVar(updateDnVars, "history_id");
                     const vars = getUpdateVarNames(updateVars, "history");
-                    setTimeout(
-                        () =>
-                            dispatch(
-                                createRequestUpdateAction(
-                                    id,
-                                    module,
-                                    vars,
-                                    true,
-                                    idVar ? { [idVar]: newDnId } : undefined
-                                )
-                            ),
-                        1
+                    Promise.resolve().then(() =>
+                        dispatch(
+                            createRequestUpdateAction(id, module, vars, true, idVar ? { [idVar]: newDnId } : undefined)
+                        )
                     );
                     return true;
                 }
@@ -392,18 +382,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                 if (showData && tabValue == TabValues.Data && dn[DataNodeFullProps.data][DatanodeDataProps.tabular]) {
                     const idVar = getUpdateVar(updateDnVars, "data_id");
                     const vars = getUpdateVarNames(updateVars, "tabularData", "tabularColumns");
-                    setTimeout(
-                        () =>
-                            dispatch(
-                                createRequestUpdateAction(
-                                    id,
-                                    module,
-                                    vars,
-                                    true,
-                                    idVar ? { [idVar]: newDnId } : undefined
-                                )
-                            ),
-                        1
+                    Promise.resolve().then(() =>
+                        dispatch(
+                            createRequestUpdateAction(id, module, vars, true, idVar ? { [idVar]: newDnId } : undefined)
+                        )
                     );
                     return true;
                 }
@@ -413,18 +395,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                 if ((req || !showData) && tabValue == TabValues.Properties) {
                     const idVar = getUpdateVar(updateDnVars, "properties_id");
                     const vars = getUpdateVarNames(updateVars, "properties");
-                    setTimeout(
-                        () =>
-                            dispatch(
-                                createRequestUpdateAction(
-                                    id,
-                                    module,
-                                    vars,
-                                    true,
-                                    idVar ? { [idVar]: newDnId } : undefined
-                                )
-                            ),
-                        1
+                    Promise.resolve().then(() =>
+                        dispatch(
+                            createRequestUpdateAction(id, module, vars, true, idVar ? { [idVar]: newDnId } : undefined)
+                        )
                     );
                     return true;
                 }

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

@@ -11,8 +11,13 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useEffect, useState, useCallback, useMemo, MouseEvent } from "react";
-import { DeleteOutline, StopCircleOutlined, Add, FilterList } from "@mui/icons-material";
+import React, { useEffect, useState, useCallback, useMemo, MouseEvent, useRef } from "react";
+import Add from "@mui/icons-material/Add";
+import CloseIcon from "@mui/icons-material/Close";
+import DeleteOutline from "@mui/icons-material/DeleteOutline";
+import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
+import FilterList from "@mui/icons-material/FilterList";
+import StopCircleOutlined from "@mui/icons-material/StopCircleOutlined";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import Checkbox from "@mui/material/Checkbox";
@@ -47,8 +52,26 @@ import {
     useModule,
 } from "taipy-gui";
 
-import { disableColor, popoverOrigin, useClassNames } from "./utils";
+import {
+    disableColor,
+    getUpdateVarNames,
+    popoverOrigin,
+    useClassNames,
+    EllipsisSx,
+    SecondaryEllipsisProps,
+} from "./utils";
 import StatusChip, { Status } from "./StatusChip";
+import JobViewer, { JobDetail } from "./JobViewer";
+import { Dialog, DialogActions, DialogContent, DialogTitle, Theme } from "@mui/material";
+
+const CloseDialogSx = {
+    position: "absolute",
+    right: 8,
+    top: 8,
+    color: (theme: Theme) => theme.palette.grey[500],
+};
+
+const RightButtonSx = { marginLeft: "auto ! important" };
 
 interface JobSelectorProps {
     updateVarName?: string;
@@ -75,10 +98,12 @@ interface JobSelectorProps {
     defaultValue?: string;
     propagate?: boolean;
     updateJbVars?: string;
+    details?: JobDetail;
+    onDetails?: string | boolean;
 }
 
-// job id, job name, empty list, entity id, entity name, submit id, creation date, status
-type Job = [string, string, [], string, string, string, string, number];
+// job id, job name, empty list, entity id, entity name, submit id, creation date, status, not deletable, not readable, not editable
+type Job = [string, string, [], string, string, string, string, number, string, string, string];
 type Jobs = Array<Job>;
 
 enum JobProps {
@@ -90,6 +115,9 @@ enum JobProps {
     submission_id,
     creation_date,
     status,
+    not_deletable,
+    not_readable,
+    not_editable,
 }
 const JobLength = Object.keys(JobProps).length / 2;
 
@@ -356,6 +384,7 @@ interface JobSelectedTableRowProps {
     handleCheckboxClick: (event: React.MouseEvent<HTMLElement>) => void;
     handleCancelJobs: (event: React.MouseEvent<HTMLElement>) => void;
     handleDeleteJobs: (event: React.MouseEvent<HTMLElement>) => void;
+    handleShowDetails: false | ((event: React.MouseEvent<HTMLElement>) => void);
     showId?: boolean;
     showSubmittedLabel?: boolean;
     showSubmittedId?: boolean;
@@ -372,13 +401,14 @@ const JobSelectedTableRow = ({
     handleCheckboxClick,
     handleCancelJobs,
     handleDeleteJobs,
+    handleShowDetails,
     showId,
     showSubmittedLabel,
     showSubmittedId,
     showSubmissionId,
     showDate,
     showCancel,
-    showDelete
+    showDelete,
 }: JobSelectedTableRowProps) => {
     const [id, jobName, , entityId, entityName, submitId, creationDate, status] = row;
 
@@ -397,18 +427,22 @@ const JobSelectedTableRow = ({
             </TableCell>
             {showId ? (
                 <TableCell component="th" scope="row" padding="none">
-                    <ListItemText primary={jobName} secondary={id} />
+                    <ListItemText primary={jobName} secondary={id} secondaryTypographyProps={SecondaryEllipsisProps} />
                 </TableCell>
             ) : null}
             {showSubmissionId ? <TableCell>{submitId}</TableCell> : null}
             {showSubmittedLabel || showSubmittedId ? (
                 <TableCell>
                     {!showSubmittedLabel && showSubmittedId ? (
-                        entityId
+                        <Typography sx={EllipsisSx}>{entityId}</Typography>
                     ) : !showSubmittedId && showSubmittedLabel ? (
-                        entityName
+                        <Typography>{entityName}</Typography>
                     ) : (
-                        <ListItemText primary={entityName} secondary={entityId} />
+                        <ListItemText
+                            primary={entityName}
+                            secondary={entityId}
+                            secondaryTypographyProps={SecondaryEllipsisProps}
+                        />
                     )}
                 </TableCell>
             ) : null}
@@ -416,8 +450,15 @@ const JobSelectedTableRow = ({
             <TableCell>
                 <StatusChip status={status} />
             </TableCell>
-            {showCancel || showDelete ? (
+            {showCancel || showDelete || handleShowDetails ? (
                 <TableCell>
+                    {handleShowDetails ? (
+                        <Tooltip title="Show details">
+                            <IconButton data-id={id} onClick={handleShowDetails}>
+                                <DescriptionOutlinedIcon />
+                            </IconButton>
+                        </Tooltip>
+                    ) : null}
                     {status === Status.RUNNING ? null : status === Status.BLOCKED ||
                       status === Status.PENDING ||
                       status === Status.SUBMITTED ? (
@@ -452,13 +493,15 @@ const JobSelector = (props: JobSelectorProps) => {
         showCancel = true,
         showDelete = true,
         propagate = true,
-        updateJbVars = ""
+        updateJbVars = "",
     } = props;
     const [checked, setChecked] = useState<string[]>([]);
     const [selected, setSelected] = useState<string[]>([]);
     const [jobRows, setJobRows] = useState<Jobs>([]);
     const [filters, setFilters] = useState<FilterData[]>();
-    const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
+    const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
+    const [showDetails, setShowDetails] = useState(false);
+    const detailId = useRef<string>();
 
     const dispatch = useDispatch();
     const module = useModule();
@@ -583,7 +626,7 @@ const JobSelector = (props: JobSelectorProps) => {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                         id: multiple === false ? [id] : JSON.parse(id),
                         action: "cancel",
-                        error_id: getUpdateVar(updateJbVars, "error_id")
+                        error_id: getUpdateVar(updateJbVars, "error_id"),
                     })
                 );
             } catch (e) {
@@ -602,7 +645,7 @@ const JobSelector = (props: JobSelectorProps) => {
                     createSendActionNameAction(props.id, module, props.onJobAction, {
                         id: multiple === false ? [id] : JSON.parse(id),
                         action: "delete",
-                        error_id: getUpdateVar(updateJbVars, "error_id")
+                        error_id: getUpdateVar(updateJbVars, "error_id"),
                     })
                 );
             } catch (e) {
@@ -612,6 +655,39 @@ const JobSelector = (props: JobSelectorProps) => {
         [dispatch, module, props.id, props.onJobAction, updateJbVars]
     );
 
+    const deleteJob = useCallback(
+        (event: React.MouseEvent<HTMLElement>) => {
+            handleDeleteJobs(event);
+            setShowDetails(false);
+        },
+        [handleDeleteJobs]
+    );
+
+    const handleShowDetails = useCallback(
+        (event: React.MouseEvent<HTMLElement>) => {
+            event.stopPropagation();
+            const { id = "" } = event.currentTarget?.dataset || {};
+            if (props.onDetails) {
+                dispatch(createSendActionNameAction(props.id, module, props.onDetails, id));
+            } else {
+                const idVar = getUpdateVar(updateJbVars, "detail_id");
+                detailId.current = id;
+                dispatch(
+                    createRequestUpdateAction(
+                        id,
+                        module,
+                        getUpdateVarNames(props.updateVars, "details"),
+                        true,
+                        idVar ? { [idVar]: id } : undefined
+                    )
+                );
+            }
+        },
+        [dispatch, module, props.id, props.onDetails, props.updateVars, updateJbVars]
+    );
+
+    const closeDetails = useCallback(() => setShowDetails(false), []);
+
     const allowCancelJobs = useMemo(
         () =>
             !!checked.length &&
@@ -650,6 +726,13 @@ const JobSelector = (props: JobSelectorProps) => {
         setAnchorEl(null);
     }, []);
 
+    useEffect(() => {
+        if (props.details && props.details[0] == detailId.current) {
+            // show Dialog
+            setShowDetails(true);
+        }
+    }, [props.details]);
+
     useEffect(() => {
         let filteredJobRows = [...(props.jobs || [])];
         filteredJobRows.length &&
@@ -705,6 +788,25 @@ const JobSelector = (props: JobSelectorProps) => {
 
     return (
         <Box className={className}>
+            {showDetails && props.details ? (
+                <Dialog open={true} onClose={closeDetails} scroll="paper" fullWidth>
+                    <DialogTitle>{props.details[1]}</DialogTitle>
+                    <IconButton aria-label="close" onClick={closeDetails} sx={CloseDialogSx}>
+                        <CloseIcon />
+                    </IconButton>
+                    <DialogContent dividers>
+                        <JobViewer job={props.details} inDialog={true}></JobViewer>
+                    </DialogContent>
+                    <DialogActions>
+                        <Button variant="outlined" color="primary" onClick={deleteJob} data-id={props.details[0]}>
+                            Delete
+                        </Button>
+                        <Button variant="outlined" color="secondary" onClick={closeDetails} sx={RightButtonSx}>
+                            Close
+                        </Button>
+                    </DialogActions>
+                </Dialog>
+            ) : null}
             <Paper sx={containerSx}>
                 <Toolbar sx={headerToolbarSx}>
                     <Grid container spacing={2} alignItems="center">
@@ -789,6 +891,7 @@ const JobSelector = (props: JobSelectorProps) => {
                                     key={row[JobProps.id]}
                                     handleDeleteJobs={handleDeleteJobs}
                                     handleCancelJobs={handleCancelJobs}
+                                    handleShowDetails={props.onDetails === false ? false : handleShowDetails}
                                     showSubmissionId={showSubmissionId}
                                     showId={showId}
                                     showSubmittedLabel={showSubmittedLabel}

+ 186 - 0
frontend/taipy/src/JobViewer.tsx

@@ -0,0 +1,186 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React, { useEffect, useCallback } from "react";
+import Button from "@mui/material/Button";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import ListItemText from "@mui/material/ListItemText";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+
+import {
+    createRequestUpdateAction,
+    createSendActionNameAction,
+    getUpdateVar,
+    useDispatch,
+    useDispatchRequestUpdateOnFirstRender,
+    useModule,
+} from "taipy-gui";
+
+import { useClassNames, EllipsisSx, SecondaryEllipsisProps } from "./utils";
+import StatusChip from "./StatusChip";
+
+interface JobViewerProps {
+    updateVarName?: string;
+    coreChanged?: Record<string, unknown>;
+    error?: string;
+    job: JobDetail;
+    onDelete?: string;
+    id?: string;
+    libClassName?: string;
+    className?: string;
+    dynamicClassName?: string;
+    updateJbVars?: string;
+    inDialog?: boolean;
+    width?: string;
+}
+
+// job id, job name, entity id, entity name, submit id, creation date, status, not deletable, execution time, logs
+export type JobDetail = [string, string, string, string, string, string, number, string, string, string[]];
+const invalidJob: JobDetail = ["", "", "", "", "", "", 0, "", "", []];
+
+const JobViewer = (props: JobViewerProps) => {
+    const { updateVarName = "", id = "", updateJbVars = "", inDialog = false, width = "50vw" } = props;
+
+    const [
+        jobId,
+        jobName,
+        entityId,
+        entityName,
+        submissionId,
+        creationDate,
+        status,
+        notDeleteable,
+        executionTime,
+        stacktrace,
+    ] = props.job || invalidJob;
+
+    const dispatch = useDispatch();
+    const module = useModule();
+
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+
+    useDispatchRequestUpdateOnFirstRender(dispatch, id, module, undefined, updateVarName);
+
+    const handleDeleteJob = useCallback(
+        (event: React.MouseEvent<HTMLElement>) => {
+            event.stopPropagation();
+            try {
+                dispatch(
+                    createSendActionNameAction(props.id, module, props.onDelete, {
+                        id: jobId,
+                        action: "delete",
+                        error_id: getUpdateVar(updateJbVars, "error_id"),
+                    })
+                );
+            } catch (e) {
+                console.warn("Error parsing ids for delete.", e);
+            }
+        },
+        [jobId, dispatch, module, props.id, props.onDelete, updateJbVars]
+    );
+
+    useEffect(() => {
+        if (props.coreChanged?.job == jobId) {
+            updateVarName && dispatch(createRequestUpdateAction(id, module, [updateVarName], true));
+        }
+    }, [props.coreChanged, updateVarName, jobId, module, dispatch, id]);
+
+    return (
+        <Grid container className={className} sx={{ maxWidth: width }}>
+            {inDialog ? null : (
+                <>
+                    <Grid item xs={4}>
+                        <Typography>Job Name</Typography>
+                    </Grid>
+                    <Grid item xs={8}>
+                        <Typography>{jobName}</Typography>
+                    </Grid>
+                    <Divider />
+                </>
+            )}
+            <Grid item xs={4}>
+                <Typography>Job Id</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <Tooltip title={jobId}>
+                    <Typography sx={EllipsisSx}>{jobId}</Typography>
+                </Tooltip>
+            </Grid>
+            <Grid item xs={4}>
+                <Typography>Submission Id</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <Tooltip title={submissionId}>
+                    <Typography sx={EllipsisSx}>{submissionId}</Typography>
+                </Tooltip>
+            </Grid>
+            <Grid item xs={4}>
+                <Typography>Submitted entity</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <Tooltip title={entityId}>
+                    <ListItemText
+                        primary={entityName}
+                        secondary={entityId}
+                        secondaryTypographyProps={SecondaryEllipsisProps}
+                    />
+                </Tooltip>
+            </Grid>
+            <Grid item xs={4}>
+                <Typography>Execution time</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <Typography>{executionTime}</Typography>
+            </Grid>
+            <Grid item xs={4}>
+                <Typography>Status</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <StatusChip status={status} />
+            </Grid>
+            <Grid item xs={4}>
+                <Typography>Creation date</Typography>
+            </Grid>
+            <Grid item xs={8}>
+                <Typography>{creationDate ? new Date(creationDate).toLocaleString() : ""}</Typography>
+            </Grid>
+            <Divider />
+            <Grid item xs={12}>
+                <Typography>Stack Trace</Typography>
+            </Grid>
+            <Grid item xs={12}>
+                <Typography variant="caption" component="pre" overflow="auto" maxHeight="50vh">
+                    {stacktrace.join("<br/>")}
+                </Typography>
+            </Grid>
+            {props.onDelete ? (
+                <>
+                    <Divider />
+                    <Grid item xs={6}>
+                        <Tooltip title={notDeleteable}>
+                            <span>
+                                <Button variant="outlined" onClick={handleDeleteJob} disabled={!!notDeleteable}>
+                                    Delete
+                                </Button>
+                            </span>
+                        </Tooltip>
+                    </Grid>
+                </>
+            ) : null}
+        </Grid>
+    );
+};
+
+export default JobViewer;

+ 20 - 16
frontend/taipy/src/utils.ts

@@ -16,22 +16,22 @@ import { PopoverOrigin } from "@mui/material/Popover";
 import { getUpdateVar, useDynamicProperty } from "taipy-gui";
 
 export type ScenarioFull = [
-    string,     // id
-    boolean,    // is_primary
-    string,     // config_id
-    string,     // creation_date
-    string,     // cycle label
-    string,     // label
-    string[],   // tags
-    Array<[string, string]>,    // properties
-    Array<[string, string[], string, string]>,   // sequences (label, task ids, notSubmittableReason, notEditableReason)
+    string, // id
+    boolean, // is_primary
+    string, // config_id
+    string, // creation_date
+    string, // cycle label
+    string, // label
+    string[], // tags
+    Array<[string, string]>, // properties
+    Array<[string, string[], string, string]>, // sequences (label, task ids, notSubmittableReason, notEditableReason)
     Record<string, string>, // tasks (id: label)
-    string[],   // authorized_tags
-    string,    // notDeletableReason
-    string,    // notPromotableReason
-    string,     // notSubmittableReason
-    string,     // notReadableReason
-    string      // notEditableReason
+    string[], // authorized_tags
+    string, // notDeletableReason
+    string, // notPromotableReason
+    string, // notSubmittableReason
+    string, // notReadableReason
+    string // notEditableReason
 ];
 
 export enum ScFProps {
@@ -218,4 +218,8 @@ export const DeleteIconSx = { height: 50, width: 50, p: 0 };
 
 export const EmptyArray = [];
 
-export const getUpdateVarNames = (updateVars: string, ...vars: string[]) => vars.map((v) => getUpdateVar(updateVars, v) || "").filter(v => v);
+export const getUpdateVarNames = (updateVars: string, ...vars: string[]) =>
+    vars.map((v) => getUpdateVar(updateVars, v) || "").filter((v) => v);
+
+export const EllipsisSx = { textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" };
+export const SecondaryEllipsisProps = { sx: EllipsisSx };

+ 9 - 4
taipy/config/config.py

@@ -207,10 +207,15 @@ class Config:
 
     @classmethod
     def _override_env_file(cls):
-        if config_filename := os.environ.get(cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH):
-            cls.__logger.info(f"Loading configuration provided by environment variable. Filename: '{config_filename}'")
-            cls._env_file_config = cls._serializer._read(config_filename)
-            cls.__logger.info(f"Configuration '{config_filename}' successfully loaded.")
+        if cfg_filename := os.environ.get(cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH):
+            if not os.path.exists(cfg_filename):
+                cls.__logger.error(f"File '{cfg_filename}' provided by environment variable "
+                                   f"'{cls._ENVIRONMENT_VARIABLE_NAME_WITH_CONFIG_PATH}' does not exist. "
+                                   f"No configuration will be loaded from environment variable.")
+                return
+            cls.__logger.info(f"Loading configuration provided by environment variable. Filename: '{cfg_filename}'")
+            cls._env_file_config = cls._serializer._read(cfg_filename)
+            cls.__logger.info(f"Configuration '{cfg_filename}' successfully loaded.")
 
     @classmethod
     def _compile_configs(cls):

+ 0 - 3
taipy/config/pyproject.toml

@@ -31,6 +31,3 @@ find = {include = ["taipy", "taipy.config", "taipy.config.*", "taipy.logger", "t
 
 [project.urls]
 homepage = "https://github.com/avaiga/taipy"
-
-[tool.setuptools.data-files]
-"version" = ["version.json"]

+ 1 - 1
taipy/config/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 1 - 1
taipy/core/common/warn_if_inputs_not_ready.py

@@ -34,7 +34,7 @@ def _warn_if_inputs_not_ready(inputs: Iterable[DataNode]):
             ]:
                 logger.warning(
                     f"{dn.id} cannot be read because it has never been written. "
-                    f"Hint: The data node may refer to a wrong path : {dn.path} "
+                    f"Hint: The data node may refer to a wrong path : {dn.properties['path']} "
                 )
             else:
                 logger.warning(f"{dn.id} cannot be read because it has never been written.")

+ 35 - 0
taipy/core/config/checkers/_scenario_config_checker.py

@@ -46,6 +46,31 @@ class _ScenarioConfigChecker(_ConfigChecker):
     def _check_if_children_config_id_is_overlapping_with_properties(
         self, scenario_config_id: str, scenario_config: ScenarioConfig
     ):
+        if scenario_config.sequences:
+            for sequence in scenario_config.sequences:
+                if sequence in scenario_config.properties:
+                    self._error(
+                        sequence,
+                        scenario_config.sequences[sequence],
+                        f"The sequence name `{sequence}` is overlapping with the "
+                        f"property `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                    )
+                if scenario_config.data_nodes:
+                    if sequence in [dn.id for dn in scenario_config.data_nodes if isinstance(dn, DataNodeConfig)]:
+                        self._error(
+                            sequence,
+                            scenario_config.sequences[sequence],
+                            f"The sequence name `{sequence}` is overlapping with the "
+                            f"data node `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
+                if scenario_config.tasks:
+                    if sequence in [task.id for task in scenario_config.tasks if isinstance(task, TaskConfig)]:
+                        self._error(
+                            sequence,
+                            scenario_config.sequences[sequence],
+                            f"The sequence name `{sequence}` is overlapping with the "
+                            f"task `{sequence}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
         if scenario_config.tasks:
             for task in scenario_config.tasks:
                 if isinstance(task, TaskConfig) and task.id in scenario_config.properties:
@@ -55,6 +80,16 @@ class _ScenarioConfigChecker(_ConfigChecker):
                         f"The id of the TaskConfig `{task.id}` is overlapping with the "
                         f"property `{task.id}` of ScenarioConfig `{scenario_config_id}`.",
                     )
+                if scenario_config.data_nodes:
+                    if isinstance(task, TaskConfig) and task.id in [
+                        dn.id for dn in scenario_config.data_nodes if isinstance(dn, DataNodeConfig)
+                    ]:
+                        self._error(
+                            TaskConfig._ID_KEY,
+                            task.id,
+                            f"The id of the TaskConfig `{task.id}` is overlapping with the "
+                            f"data node `{task.id}` of ScenarioConfig `{scenario_config_id}`.",
+                        )
         if scenario_config.data_nodes:
             for data_node in scenario_config.data_nodes:
                 if isinstance(data_node, DataNodeConfig) and data_node.id in scenario_config.properties:

+ 0 - 6
taipy/core/cycle/cycle.py

@@ -211,12 +211,6 @@ class Cycle(_Entity, _Labeled):
 
         return CycleId(_get_valid_filename(Cycle.__SEPARATOR.join([Cycle._ID_PREFIX, name, str(uuid.uuid4())])))
 
-    def __getattr__(self, attribute_name):
-        protected_attribute_name = attribute_name
-        if protected_attribute_name in self._properties:
-            return self._properties[protected_attribute_name]
-        raise AttributeError(f"{attribute_name} is not an attribute of cycle {self.id}")
-
     def __eq__(self, other):
         return isinstance(other, Cycle) and self.id == other.id
 

+ 2 - 8
taipy/core/data/data_node.py

@@ -134,7 +134,7 @@ class DataNode(_Entity, _Labeled):
 
     _ID_PREFIX = "DATANODE"
     __ID_SEPARATOR = "_"
-    __logger = _TaipyLogger._get_logger()
+    _logger = _TaipyLogger._get_logger()
     _REQUIRED_PROPERTIES: List[str] = []
     _MANAGER_NAME: str = "data"
     _PATH_KEY = "path"
@@ -347,12 +347,6 @@ class DataNode(_Entity, _Labeled):
     def __setstate__(self, state):
         vars(self).update(state)
 
-    def __getattr__(self, attribute_name):
-        protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return self._properties[protected_attribute_name]
-        raise AttributeError(f"{attribute_name} is not an attribute of data node {self.id}")
-
     @classmethod
     def _get_last_modified_datetime(cls, path: Optional[str] = None) -> Optional[datetime]:
         if path and os.path.isfile(path):
@@ -397,7 +391,7 @@ class DataNode(_Entity, _Labeled):
         try:
             return self.read_or_raise()
         except NoData:
-            self.__logger.warning(
+            self._logger.warning(
                 f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             return None

+ 1 - 1
taipy/core/data/parquet.py

@@ -189,7 +189,7 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
 
         # return None if data was never written
         if not self.last_edit_date:
-            self._DataNode__logger.warning(
+            self._logger.warning(
                 f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             return None

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

@@ -383,3 +383,10 @@ class SQLQueryCannotBeExecuted(Exception):
 
 class _SuspiciousFileOperation(Exception):
     pass
+
+
+class AttributeKeyAlreadyExisted(Exception):
+    """Raised when an attribute key already existed."""
+
+    def __init__(self, key: str):
+        self.message = f"Attribute key '{key}' already existed."

+ 0 - 3
taipy/core/pyproject.toml

@@ -36,6 +36,3 @@ find = {include = ["taipy", "taipy.core", "taipy.core.*"]}
 
 [project.urls]
 homepage = "https://github.com/avaiga/taipy"
-
-[tool.setuptools.data-files]
-"version" = ["version.json"]

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

@@ -268,12 +268,9 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return None
 
     @classmethod
-    def _get_by_tag(cls, cycle: Cycle, tag: str) -> Optional[Scenario]:
-        scenarios = cls._get_all_by_cycle(cycle)
-        for scenario in scenarios:
-            if scenario.has_tag(tag):
-                return scenario
-        return None
+    def _get_all_by_cycle_tag(cls, cycle: Cycle, tag: str) -> List[Scenario]:
+        cycles_scenarios = cls._get_all_by_cycle(cycle)
+        return [scenario for scenario in cycles_scenarios if scenario.has_tag(tag)]
 
     @classmethod
     def _get_all_by_tag(cls, tag: str) -> List[Scenario]:
@@ -376,10 +373,6 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         tags = scenario.properties.get(cls._AUTHORIZED_TAGS_KEY, set())
         if len(tags) > 0 and tag not in tags:
             raise UnauthorizedTagError(f"Tag `{tag}` not authorized by scenario configuration `{scenario.config_id}`")
-        if scenario.cycle:
-            if old_tagged_scenario := cls._get_by_tag(scenario.cycle, tag):
-                old_tagged_scenario.remove_tag(tag)
-                cls._set(old_tagged_scenario)
         scenario._add_tag(tag)
         cls._set(scenario)
         Notifier.publish(

+ 23 - 10
taipy/core/scenario/scenario.py

@@ -16,7 +16,6 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 
 import networkx as nx
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 
 from .._entity._entity import _Entity
@@ -31,6 +30,7 @@ from ..cycle.cycle import Cycle
 from ..data.data_node import DataNode
 from ..data.data_node_id import DataNodeId
 from ..exceptions.exceptions import (
+    AttributeKeyAlreadyExisted,
     InvalidSequence,
     NonExistingDataNode,
     NonExistingSequence,
@@ -117,6 +117,7 @@ class Scenario(_Entity, Submittable, _Labeled):
     _SEQUENCE_TASKS_KEY = "tasks"
     _SEQUENCE_PROPERTIES_KEY = "properties"
     _SEQUENCE_SUBSCRIBERS_KEY = "subscribers"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -155,6 +156,7 @@ class Scenario(_Entity, Submittable, _Labeled):
             )
 
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
+        self._init_done = True
 
     @staticmethod
     def _new_id(config_id: str) -> ScenarioId:
@@ -176,20 +178,28 @@ class Scenario(_Entity, Submittable, _Labeled):
     def __eq__(self, other):
         return isinstance(other, Scenario) and self.id == other.id
 
-    def __getattr__(self, attribute_name):
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
+    def __getattr__(self, attribute_name) -> Union[Sequence, Task, DataNode]:
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
-
         sequences = self._get_sequences()
         if protected_attribute_name in sequences:
             return sequences[protected_attribute_name]
-        tasks = self.tasks
+        tasks = self.__get_tasks()
         if protected_attribute_name in tasks:
             return tasks[protected_attribute_name]
-        data_nodes = self.data_nodes
+        data_nodes = self.__get_data_nodes()
         if protected_attribute_name in data_nodes:
             return data_nodes[protected_attribute_name]
+
         raise AttributeError(f"{attribute_name} is not an attribute of scenario {self.id}")
 
     @property
@@ -458,14 +468,17 @@ class Scenario(_Entity, Submittable, _Labeled):
     def _get_set_of_tasks(self) -> Set[Task]:
         return set(self.tasks.values())
 
-    @property  # type: ignore
-    @_self_reload(_MANAGER_NAME)
-    def data_nodes(self) -> Dict[str, DataNode]:
+    def __get_data_nodes(self) -> Dict[str, DataNode]:
         data_nodes_dict = self.__get_additional_data_nodes()
         for _, task in self.__get_tasks().items():
             data_nodes_dict.update(task.data_nodes)
         return data_nodes_dict
 
+    @property  # type: ignore
+    @_self_reload(_MANAGER_NAME)
+    def data_nodes(self) -> Dict[str, DataNode]:
+        return self.__get_data_nodes()
+
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def creation_date(self):

+ 13 - 4
taipy/core/sequence/sequence.py

@@ -15,7 +15,6 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
 
 import networkx as nx
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 
 from .._entity._entity import _Entity
@@ -27,7 +26,7 @@ from .._version._version_manager_factory import _VersionManagerFactory
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..data.data_node import DataNode
-from ..exceptions.exceptions import NonExistingTask
+from ..exceptions.exceptions import AttributeKeyAlreadyExisted, NonExistingTask
 from ..job.job import Job
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..submission.submission import Submission
@@ -126,6 +125,7 @@ class Sequence(_Entity, Submittable, _Labeled):
     _ID_PREFIX = "SEQUENCE"
     _SEPARATOR = "_"
     _MANAGER_NAME = "sequence"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -144,6 +144,7 @@ class Sequence(_Entity, Submittable, _Labeled):
         self._parent_ids = parent_ids or set()
         self._properties = _Properties(self, **properties)
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
+        self._init_done = True
 
     @staticmethod
     def _new_id(sequence_name: str, scenario_id) -> SequenceId:
@@ -156,10 +157,18 @@ class Sequence(_Entity, Submittable, _Labeled):
     def __eq__(self, other):
         return isinstance(other, Sequence) and self.id == other.id
 
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
     def __getattr__(self, attribute_name):
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
         tasks = self._get_tasks()
         if protected_attribute_name in tasks:
             return tasks[protected_attribute_name]

+ 2 - 4
taipy/core/taipy.py

@@ -548,8 +548,7 @@ def get_scenarios(
     elif not cycle and tag:
         scenarios = scenario_manager._get_all_by_tag(tag)
     elif cycle and tag:
-        cycles_scenarios = scenario_manager._get_all_by_cycle(cycle)
-        scenarios = [scenario for scenario in cycles_scenarios if scenario.has_tag(tag)]
+        scenarios = scenario_manager._get_all_by_cycle_tag(cycle, tag)
     else:
         scenarios = []
 
@@ -640,8 +639,7 @@ def set_primary(scenario: Scenario):
 def tag(scenario: Scenario, tag: str):
     """Add a tag to a scenario.
 
-    This function adds a user-defined tag to the specified scenario. If another scenario
-    within the same cycle already has the same tag applied, the previous scenario is untagged.
+    This function adds a user-defined tag to the specified scenario.
 
     Parameters:
         scenario (Scenario^): The scenario to which the tag will be added.

+ 13 - 3
taipy/core/task/task.py

@@ -12,7 +12,6 @@
 import uuid
 from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
 
-from taipy.config.common._template_handler import _TemplateHandler as _tpl
 from taipy.config.common._validate_id import _validate_id
 from taipy.config.common.scope import Scope
 
@@ -22,6 +21,7 @@ from .._entity._properties import _Properties
 from .._entity._reload import _Reloader, _self_reload, _self_setter
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..data.data_node import DataNode
+from ..exceptions import AttributeKeyAlreadyExisted
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..submission.submission import Submission
 from .task_id import TaskId
@@ -97,6 +97,7 @@ class Task(_Entity, _Labeled):
     _ID_PREFIX = "TASK"
     __ID_SEPARATOR = "_"
     _MANAGER_NAME = "task"
+    __CHECK_INIT_DONE_ATTR_NAME = "_init_done"
 
     def __init__(
         self,
@@ -121,6 +122,7 @@ class Task(_Entity, _Labeled):
         self._version = version or _VersionManagerFactory._build_manager()._get_latest_version()
         self._skippable = skippable
         self._properties = _Properties(self, **properties)
+        self._init_done = True
 
     def __hash__(self):
         return hash(self.id)
@@ -134,10 +136,18 @@ class Task(_Entity, _Labeled):
     def __setstate__(self, state):
         vars(self).update(state)
 
+    def __setattr__(self, name: str, value: Any) -> None:
+        if self.__CHECK_INIT_DONE_ATTR_NAME not in dir(self) or name in dir(self):
+            return super().__setattr__(name, value)
+        else:
+            try:
+                self.__getattr__(name)
+                raise AttributeKeyAlreadyExisted(name)
+            except AttributeError:
+                return super().__setattr__(name, value)
+
     def __getattr__(self, attribute_name):
         protected_attribute_name = _validate_id(attribute_name)
-        if protected_attribute_name in self._properties:
-            return _tpl._replace_templates(self._properties[protected_attribute_name])
         if protected_attribute_name in self.input:
             return self.input[protected_attribute_name]
         if protected_attribute_name in self.output:

+ 1 - 1
taipy/core/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 1 - 1
taipy/gui/_renderers/builder.py

@@ -357,7 +357,7 @@ class _Builder:
             if strattr is None:
                 return self
         elif _is_boolean(strattr) and not _is_true(strattr):
-            return self
+            return self.__set_react_attribute(_to_camel_case(name), False)
         elif strattr:
             strattr = str(strattr)
             func = self.__gui._get_user_function(strattr)

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

@@ -81,6 +81,7 @@ class _Factory:
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "chat": lambda gui, control_type, attrs: _Builder(
@@ -145,6 +146,7 @@ class _Factory:
                 ("label",),
                 ("on_change", PropertyType.function),
                 ("format",),
+                ("width", PropertyType.string_or_number),
             ]
         )
         ._set_propagate(),
@@ -165,6 +167,7 @@ class _Factory:
                 ("label_end",),
                 ("on_change", PropertyType.function),
                 ("format",),
+                ("width", PropertyType.string_or_number),
             ]
         )
         ._set_propagate(),
@@ -220,6 +223,7 @@ class _Factory:
                 ("bypass_preview", PropertyType.boolean, True),
                 ("name",),
                 ("hover_text", PropertyType.dynamic_string),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "file_selector": lambda gui, control_type, attrs: _Builder(
@@ -239,6 +243,7 @@ class _Factory:
                 ("drop_message",),
                 ("hover_text", PropertyType.dynamic_string),
                 ("notify", PropertyType.boolean, True),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "image": lambda gui, control_type, attrs: _Builder(
@@ -296,6 +301,7 @@ class _Factory:
                 ("change_delay", PropertyType.number, gui._get_config("change_delay", None)),
                 ("multiline", PropertyType.boolean, False),
                 ("lines_shown", PropertyType.number, 5),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "layout": lambda gui, control_type, attrs: _Builder(
@@ -541,6 +547,7 @@ class _Factory:
                 ("hover_text", PropertyType.dynamic_string),
                 ("raw", PropertyType.boolean, False),
                 ("mode",),
+                ("width", PropertyType.string_or_number),
             ]
         ),
         "toggle": lambda gui, control_type, attrs: _Builder(
@@ -558,6 +565,7 @@ class _Factory:
                 ("on_change", PropertyType.function),
                 ("mode",),
                 ("lov", PropertyType.single_lov),
+                ("width", PropertyType.string_or_number),
             ]
         )
         ._set_kind()

+ 0 - 3
taipy/gui/pyproject.toml

@@ -36,6 +36,3 @@ find = {include = ["taipy", "taipy.gui", "taipy.gui.*"]}
 
 [project.urls]
 homepage = "https://github.com/avaiga/taipy"
-
-[tool.setuptools.data-files]
-"version" = ["version.json"]

+ 1 - 1
taipy/gui/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 70 - 17
taipy/gui/viselements.json

@@ -29,6 +29,12 @@
                         "name": "format",
                         "type": "str",
                         "doc": "The format to apply to the value.<br/>See below."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the element."
                     }
                 ]
             }
@@ -50,7 +56,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button it it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
@@ -66,6 +72,12 @@
                                 "dict"
                             ]
                         ]
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the button element."
                     }
                 ]
             }
@@ -273,6 +285,17 @@
                         "name": "mode",
                         "type": "str",
                         "doc": "Define the way the toggle is displayed:\n<ul><li>&quot;theme&quot;: synonym for setting the *theme* property to True</li></ul>"
+                    },
+                    {
+                        "name": "label",
+                        "type": "str",
+                        "doc": "The label associated with the toggle."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the element."
                     }
                 ]
             }
@@ -322,6 +345,12 @@
                         "name": "max",
                         "type": "dynamic(datetime)",
                         "doc": "The maximum date to accept for this input."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the date element."
                     }
                 ]
             }
@@ -366,6 +395,12 @@
                         "name": "label_end",
                         "type": "str",
                         "doc": "The label associated with the second input."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the date_range element."
                     }
                 ]
             }
@@ -500,7 +535,7 @@
                     },
                     {
                         "name": "on_range_change",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The callback function that is invoked when the visible part of the x axis changes.<br/>The function receives three parameters:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the chart control if it has one.</li>\n<li>payload (dict[str, any]): the full details on this callback's invocation, as emitted by <a href=\"https://plotly.com/javascript/plotlyjs-events/#update-data\">Plotly</a>.</li>\n</ul>",
                         "signature": [
                             [
@@ -797,7 +832,7 @@
                     },
                     {
                         "name": "on_edit",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "TODO: Default implementation and False value. The name of a function that is triggered when a cell edition is validated.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>index (int): the row index.</li>\n<li>col (str): the column name.</li>\n<li>value (any): the new cell value cast to the type of the column.</li>\n<li>user_value (str): the new cell value, as it was provided by the user.</li>\n<li>tz (str): the timezone if the column type is date.</li>\n</ul>\n</li>\n</ul><br/>If this property is not set, the user cannot edit cells.",
                         "signature": [
                             [
@@ -816,7 +851,7 @@
                     },
                     {
                         "name": "on_delete",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "TODO: Default implementation and False value. The name of a function that is triggered when a row is deleted.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>index (int): the row index.</li>\n</ul>\n</li>\n</ul><br/>If this property is not set, the user cannot delete rows.",
                         "signature": [
                             [
@@ -835,7 +870,7 @@
                     },
                     {
                         "name": "on_add",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "TODO: Default implementation and False value. The name of a function that is triggered when the user requests a row to be added.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>index (int): the row index.</li>\n</ul>\n</li>\n</ul><br/>If this property is not set, the user cannot add rows.",
                         "signature": [
                             [
@@ -854,7 +889,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the user selects a row.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>index (int): the row index.</li>\n<li>col (str): the column name.</li>\n<li>reason (str): the origin of the action: \"click\", or \"button\" if the cell contains a Markdown link syntax.</li>\n<li>value (str): the *link value* indicated in the cell when using a Markdown link syntax (that is, <i>reason</i> is set to \"button\").</li></ul></li></ul>.",
                         "signature": [
                             [
@@ -895,7 +930,7 @@
                     },
                     {
                         "name": "on_compare",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "A data comparison function that would return a structure that identifies the differences between the different data passed as name. The default implementation compares the default data with the data[1] value.",
                         "signature": [
                             [
@@ -987,7 +1022,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the download is terminated (or on user action if <i>content</i> is None).<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has two keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: A list of two elements: <i>args[0]</i> reflects the <i>name</i> property and <i>args[1]</i> holds the file URL.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
@@ -1026,6 +1061,12 @@
                         "name": "name",
                         "type": "str",
                         "doc": "A name proposition for the file to save, that the user can change."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the element."
                     }
                 ]
             }
@@ -1051,7 +1092,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of the function that will be triggered.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
@@ -1091,6 +1132,12 @@
                         "type": "bool",
                         "default_value": "True",
                         "doc": "If set to False, the user won't be notified of upload finish."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the element."
                     }
                 ]
             }
@@ -1116,7 +1163,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the user clicks on the image.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
@@ -1406,7 +1453,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of the function that is triggered when a menu option is selected.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: List where the first element contains the id of the selected option.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
@@ -1481,7 +1528,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:\n<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
                         "signature": [
                             [
@@ -1540,7 +1587,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the user enters a new message.<br/>All the parameters of that function are optional:\n<ul>\n<li><i>state</i> (<code>State^</code>): the state instance.</li>\n<li><i>var_name</i> (str): the name of the variable bound to the <i>messages</i> property.</li>\n<li><i>payload</i> (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li><i>action</i>: the name of the action that triggered this callback.</li>\n<li><i>args</i> (list): A list composed of a reason (\"click\" or \"Enter\"), the variable name, message, the user identifier of the sender.</li></ul></li></ul>",
                         "signature": [
                             [
@@ -1686,7 +1733,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "Name of a function triggered when a button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the dialog if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list where the first element contains the index of the selected label.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
@@ -1775,7 +1822,7 @@
                     },
                     {
                         "name": "on_close",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when this pane is closed (if the user clicks outside of it or presses the Esc key).<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the close button if it has one.</li>\n</ul><br/>If this property is not set, no function is called when this pane is closed.",
                         "signature": [
                             [
@@ -1875,7 +1922,7 @@
                 "properties": [
                     {
                         "name": "on_change",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the value is updated.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the variable name.</li>\n<li>value (any): the new value.</li>\n</ul>",
                         "signature": [
                             [
@@ -1941,7 +1988,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "Name of a function that is triggered when a specific key is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the control if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args (list):\n<ul><li>key name</li><li>variable name</li><li>current value</li></ul>\n</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
@@ -1963,6 +2010,12 @@
                         "type": "str",
                         "default_value": "\"Enter\"",
                         "doc": "Semicolon (';')-separated list of supported key names.<br/>Authorized values are Enter, Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12."
+                    },
+                    {
+                        "name": "width",
+                        "type": "Union[str,int]",
+                        "default_value": "None",
+                        "doc": "The width of the element."
                     }
                 ]
             }

+ 9 - 1
taipy/gui_core/_GuiCoreLib.py

@@ -50,6 +50,7 @@ class _GuiCore(ElementLibrary):
     __SCENARIO_SELECTOR_SORT_VAR = "__tpgc_sc_sort"
     __SCENARIO_VIZ_ERROR_VAR = "__tpgc_sv_error"
     __JOB_SELECTOR_ERROR_VAR = "__tpgc_js_error"
+    __JOB_DETAIL_ID_VAR = "__tpgc_jd_id"
     __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"
@@ -283,6 +284,7 @@ class _GuiCore(ElementLibrary):
                 "show_cancel": ElementProperty(PropertyType.boolean, True),
                 "show_delete": ElementProperty(PropertyType.boolean, True),
                 "on_change": ElementProperty(PropertyType.function),
+                "on_details": ElementProperty(PropertyType.function),
                 "height": ElementProperty(PropertyType.string, "50vh"),
             },
             inner_properties={
@@ -291,8 +293,14 @@ class _GuiCore(ElementLibrary):
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
                 "error": ElementProperty(PropertyType.dynamic_string, f"{{{__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>}}"),
+                "details": ElementProperty(
+                    PropertyType.react,
+                    f"{{{__CTX_VAR_NAME}.get_job_details(" + f"{__JOB_DETAIL_ID_VAR}<tp:uniq:jb>)}}",
+                ),
                 "update_jb_vars": ElementProperty(
-                    PropertyType.string, f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>"
+                    PropertyType.string,
+                    f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>;"
+                    + f"detail_id={__JOB_DETAIL_ID_VAR}<tp:uniq:jb>;",
                 ),
             },
         ),

+ 112 - 71
taipy/gui_core/_context.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
+import datetime
 import json
 import typing as t
 from collections import defaultdict
@@ -29,6 +30,7 @@ from taipy.core import (
     DataNode,
     DataNodeId,
     Job,
+    JobId,
     Scenario,
     ScenarioId,
     Sequence,
@@ -54,11 +56,13 @@ from taipy.core import submit as core_submit
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
+from taipy.core.reason import ReasonCollection
 from taipy.core.submission.submission_status import SubmissionStatus
 from taipy.core.taipy import can_create
 from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
+from taipy.gui.utils._map_dict import _MapDict
 
 from ._adapters import (
     CustomScenarioFilter,
@@ -125,7 +129,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         else None
                     )
                     if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids:  # type: ignore
-                        self.broadcast_core_changed({"scenario": list(sequence.parent_ids)})
+                        self.broadcast_core_changed({"scenario": list(sequence.parent_ids)})  # type: ignore
             except Exception as e:
                 _warn(f"Access to sequence {event.entity_id} failed", e)
         elif event.entity_type == EventEntityType.JOB:
@@ -420,8 +424,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if update:
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             if delete:
-                if not is_deletable(scenario_id):
-                    state.assign(error_var, f"Scenario. {scenario_id} is not deletable.")
+                if not (reason := is_deletable(scenario_id)):
+                    state.assign(error_var, f"Scenario. {scenario_id} is not deletable: {_get_reason(reason)}.")
                     return
                 try:
                     core_delete(scenario_id)
@@ -507,12 +511,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
                     try:
                         var_name, _ = gui._get_real_var_name(sel_scenario_var)
-                        self.gui._update_var(var_name, scenario or user_scenario, on_change= args[2])
+                        self.gui._update_var(var_name, scenario or user_scenario, on_change=args[2])
                     except Exception as e:  # pragma: no cover
                         _warn("Can't find value variable name in context", e)
         if scenario:
-            if not is_editable(scenario):
-                state.assign(error_var, f"Scenario {scenario_id or name} is not editable.")
+            if not (reason := is_editable(scenario)):
+                state.assign(error_var, f"Scenario {scenario_id or name} is not editable: {_get_reason(reason)}.")
                 return
             with scenario as sc:
                 sc.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
@@ -555,9 +559,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     else:
                         primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                         if primary is True:
-                            if not is_promotable(scenario):
+                            if not (reason := is_promotable(scenario)):
                                 _GuiCoreContext.__assign_var(
-                                    state, error_var, f"Scenario {entity_id} is not promotable."
+                                    state, error_var, f"Scenario {entity_id} is not promotable: {_get_reason(reason)}."
                                 )
                                 return
                             set_primary(scenario)
@@ -602,7 +606,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     state,
                     error_var,
                     f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable: "
-                    + reason.reasons,
+                    + f"{_get_reason(reason)}.",
                 )
                 return
             if entity:
@@ -804,14 +808,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         job.id,
                         job.get_simple_label(),
                         [],
-                        entity.get_simple_label() if entity else "",
                         entity.id if entity else "",
+                        entity.get_simple_label() if entity else "",
                         job.submit_id,
                         job.creation_date,
                         job.status.value,
-                        is_deletable(job),
-                        is_readable(job),
-                        is_editable(job),
+                        _get_reason(is_deletable(job)),
+                        _get_reason(is_readable(job)),
+                        _get_reason(is_editable(job)),
                     )
         except Exception as e:
             _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
@@ -829,11 +833,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
             errs = []
             if job_action == "delete":
                 for job_id in job_ids:
-                    if not is_readable(job_id):
-                        errs.append(f"Job {job_id} is not readable.")
+                    if not (reason := is_readable(job_id)):
+                        errs.append(f"Job {job_id} is not readable: {_get_reason(reason)}.")
                         continue
-                    if not is_deletable(job_id):
-                        errs.append(f"Job {job_id} is not deletable.")
+                    if not (reason := is_deletable(job_id)):
+                        errs.append(f"Job {job_id} is not deletable: {_get_reason(reason)}.")
                         continue
                     try:
                         delete_job(core_get(job_id))
@@ -841,11 +845,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         errs.append(f"Error deleting job. {e}")
             elif job_action == "cancel":
                 for job_id in job_ids:
-                    if not is_readable(job_id):
-                        errs.append(f"Job {job_id} is not readable.")
+                    if not (reason := is_readable(job_id)):
+                        errs.append(f"Job {job_id} is not readable: {_get_reason(reason)}.")
                         continue
-                    if not is_editable(job_id):
-                        errs.append(f"Job {job_id} is not cancelable.")
+                    if not (reason := is_editable(job_id)):
+                        errs.append(f"Job {job_id} is not cancelable: {_get_reason(reason)}.")
                         continue
                     try:
                         cancel_job(job_id)
@@ -853,6 +857,29 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         errs.append(f"Error canceling job. {e}")
             _GuiCoreContext.__assign_var(state, payload.get("error_id"), "<br/>".join(errs) if errs else "")
 
+    def get_job_details(self, job_id: t.Optional[JobId]):
+        try:
+            if job_id and is_readable(job_id) and (job := core_get(job_id)) is not None:
+                if isinstance(job, Job):
+                    entity = core_get(job.owner_id)
+                    return (
+                        job.id,
+                        job.get_simple_label(),
+                        entity.id if entity else "",
+                        entity.get_simple_label() if entity else "",
+                        job.submit_id,
+                        job.creation_date,
+                        job.status.value,
+                        _get_reason(is_deletable(job)),
+                        ""
+                        if job.execution_duration is None
+                        else str(datetime.timedelta(seconds=job.execution_duration)),
+                        [] if job.stacktrace is None else job.stacktrace,
+                    )
+        except Exception as e:
+            _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
+        return None
+
     def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
         self.__lazy_start()
         args = payload.get("args")
@@ -947,8 +974,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 job_id = e.get("job_id")
                 job: t.Optional[Job] = None
                 if job_id:
-                    if not is_readable(job_id):
-                        job_id += " not readable"
+                    if not (reason := is_readable(job_id)):
+                        job_id += f" is not readable: {_get_reason(reason)}."
                     else:
                         job = core_get(job_id)
                 res.append(
@@ -964,11 +991,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return _DoNotUpdate()
 
     def __check_readable_editable(self, state: State, id: str, ent_type: str, var: t.Optional[str]):
-        if not is_readable(t.cast(ScenarioId, id)):
-            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable.")
+        if not (reason := is_readable(t.cast(ScenarioId, id))):
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable: {_get_reason(reason)}.")
             return False
-        if not is_editable(t.cast(ScenarioId, id)):
-            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable.")
+        if not (reason := is_editable(t.cast(ScenarioId, id))):
+            _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable: {_get_reason(reason)}.")
             return False
         return True
 
@@ -1001,7 +1028,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 _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):  # noqa:C901
         self.__lazy_start()
         error_var = payload.get("error_id")
         user_data = payload.get("user_data", {})
@@ -1028,6 +1055,23 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     elif isinstance(data, pd.Series):
                         data.at[idx] = val
                     new_data = data
+                elif isinstance(data, (dict, _MapDict)):
+                    row = data.get(col, None)
+                    data_tuple = False
+                    if isinstance(row, tuple):
+                        row = list(row)
+                        data_tuple = True
+                    if isinstance(row, list):
+                        row[idx] = val
+                        if data_tuple:
+                            data[col] = tuple(row)
+                        new_data = data
+                    else:
+                        _GuiCoreContext.__assign_var(
+                            state,
+                            error_var,
+                            "Error updating Datanode: dict values must be list or tuple.",
+                        )
                 else:
                     data_tuple = False
                     if isinstance(data, tuple):
@@ -1088,55 +1132,44 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def get_data_node_tabular_data(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                value = self.__read_tabular_data(dn)
-                if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
-                    return value
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    value = self.__read_tabular_data(dn)
+                    if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
+                        return value
+                except Exception:
+                    return None
         return None
 
     def get_data_node_tabular_columns(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                value = self.__read_tabular_data(dn)
-                if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
-                    return self.gui._tbl_cols(
-                        True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=value
-                    )
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    value = self.__read_tabular_data(dn)
+                    if _GuiCoreDatanodeAdapter._is_tabular_data(dn, value):
+                        return self.gui._tbl_cols(
+                            True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=value
+                        )
+                except Exception:
+                    return None
         return None
 
     def get_data_node_chart_config(self, id: str):
         self.__lazy_start()
-        if (
-            id
-            and is_readable(t.cast(DataNodeId, id))
-            and (dn := core_get(id))
-            and isinstance(dn, DataNode)
-            and dn.is_ready_for_reading
-        ):
-            try:
-                return self.gui._chart_conf(
-                    True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=self.__read_tabular_data(dn)
-                )
-            except Exception:
-                return None
+        if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
+            if dn.is_ready_for_reading or (dn.edit_in_progress and dn.editor_id == self.gui._get_client_id()):
+                try:
+                    return self.gui._chart_conf(
+                        True,
+                        True,
+                        "{}",
+                        json.dumps({"data": "tabular_data"}),
+                        tabular_data=self.__read_tabular_data(dn),
+                    )
+                except Exception:
+                    return None
         return None
 
     def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]):
@@ -1147,7 +1180,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
         on_action_function = self.gui._get_user_function(args[1]) if args[1] else None
         if callable(on_action_function):
             try:
-                entity = core_get(args[0]) if is_readable(args[0]) else f"unredable({args[0]})"
+                entity = (
+                    core_get(args[0])
+                    if (reason := is_readable(args[0]))
+                    else f"{args[0]} is not readable: {_get_reason(reason)}"
+                )
                 self.gui._call_function_with_state(
                     on_action_function,
                     [entity],
@@ -1160,4 +1197,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def get_creation_reason(self):
         self.__lazy_start()
-        return "" if (reason := can_create()) else f"Cannot create scenario: {reason.reasons}"
+        return "" if (reason := can_create()) else f"Cannot create scenario: {_get_reason(reason)}"
+
+
+def _get_reason(reason: t.Union[bool, ReasonCollection]):
+    return reason.reasons if isinstance(reason, ReasonCollection) else " "

+ 27 - 7
taipy/gui_core/viselements.json

@@ -33,7 +33,7 @@
                     },
                     {
                         "name": "on_change",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the value is updated.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the variable name.</li>\n<li>value (<code>Scenario^</code>): the selected scenario.</li>\n</ul>",
                         "signature": [
                             [
@@ -64,8 +64,8 @@
                     },
                     {
                         "name": "on_creation",
-                        "type": "Callback",
-                        "doc": "The name of the function that is triggered when a scenario is about to be created.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the scenario selector.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>config: the name of the selected scenario configuration.</li>\n<li>date: the creation date for the new scenario.</li>\n<li>label: the user-specified label.</li>\n<li>properties: a dictionary containing all the user-defined custom properties.</li>\n</ul>\n</li>\n<li>The callback function can return a scenario, a string containing an error message (a scenario will not be created), or None (then a new scenario is created with the user parameters).</li>\n</ul>",
+                        "type": "Callable",
+                        "doc": "The name of the function that is triggered when a scenario is about to be created.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of this scenario selector.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>config (str): the name of the selected scenario configuration.</li>\n<li>date (datetime): the creation date for the new scenario.</li>\n<li>label (str): the user-specified label.</li>\n<li>properties (dic): a dictionary containing all the user-defined custom properties.</li>\n</ul>\n</li>\n<li>The callback function can return a scenario, a string containing an error message (a scenario will not be created), or None (then a new scenario is created with the user parameters).</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -207,7 +207,7 @@
                     {
                         "name": "on_submission_change",
                         "type": "Callback",
-                        "doc": "The name of the function that is triggered when a submission status is changed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>submission (Submission): the submission entity containing submission information.</li>\n<li>details (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>submission_status (str): the new status of the submission (possible values: SUBMITTED, COMPLETED, CANCELED, FAILED, BLOCKED, WAITING, RUNNING).</li>\n<li>job: the Job (if any) that is at the origin of the submission status change.</li>\n<li>submittable_entity: submittable (Submittable): the entity (usually a Scenario) that was submitted.</li>\n</ul>",
+                        "doc": "The name of the function that is triggered when a submission status is changed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>submission (Submission): the submission entity containing submission information.</li>\n<li>details (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>submission_status (str): the new status of the submission (possible values are: \"SUBMITTED\", \"COMPLETED\", \"CANCELED\", \"FAILED\", \"BLOCKED\", \"WAITING\", or \"RUNNING\").</li>\n<li>job: the Job (if any) that is at the origin of the submission status change.</li>\n<li>submittable_entity (Submittable): the entity (usually a Scenario) that was submitted.</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -265,7 +265,7 @@
                     },
                     {
                         "name": "on_action",
-                        "type": "Callback",
+                        "type": "Callable",
                         "doc": "The name of the function that is triggered when a a node is selected.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>entity (DataNode | Task): the entity (DataNode or Task) that was selected.</li>\n</ul>",
                         "signature": [
                             [
@@ -308,7 +308,7 @@
                     },
                     {
                         "name": "on_change",
-                        "type": "callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when a data node is selected.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the variable name.</li>\n<li>value (<code>DataNode^</code>): the selected data node.</li>\n</ul>",
                         "signature": [
                             [
@@ -517,7 +517,7 @@
                     },
                     {
                         "name": "on_change",
-                        "type": "callback",
+                        "type": "Callable",
                         "doc": "The name of a function that is triggered when the selection is updated.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the variable name.</li>\n<li>value (<code>Job^</code>): the selected job.</li>\n</ul>",
                         "signature": [
                             [
@@ -539,6 +539,26 @@
                         "type": "str",
                         "default_value": "\"50vh\"",
                         "doc": "The maximum height, in CSS units, of the control."
+                    },
+                    {
+                        "name": "on_details",
+                        "type": "Union[Callback, bool]",
+                        "doc": "The name of a function that is triggered when the details icon is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the id of the control.</li>\n<li>payload (<code>dict^</code>): a dictionary that contains the Job Id in the value for key <i>args<i>.</li>\n</ul></br>If False, the icon is not shown.",
+                        "signature": [
+                            [
+                                "state",
+                                "State"
+                            ],
+                            [
+                                "id",
+                                "str"
+                            ],
+                            [
+                                "payload",
+                                "dict"
+                            ]
+                        ]
+
                     }
                 ]
             }

+ 0 - 3
taipy/rest/pyproject.toml

@@ -28,6 +28,3 @@ find = {include = ["taipy", "taipy.rest"]}
 
 [project.urls]
 homepage = "https://github.com/avaiga/taipy"
-
-[tool.setuptools.data-files]
-"version" = ["version.json"]

+ 1 - 1
taipy/rest/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 0 - 3
taipy/templates/pyproject.toml

@@ -28,6 +28,3 @@ find = {include = ["taipy"]}
 
 [project.urls]
 homepage = "https://github.com/avaiga/taipy"
-
-[tool.setuptools.data-files]
-"version" = ["version.json"]

+ 1 - 1
taipy/templates/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 1 - 1
taipy/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 0, "ext": "dev0"}
+{"major": 4, "minor": 0, "patch": 0, "ext": "dev1"}

+ 69 - 11
tests/core/config/checkers/test_scenario_config_checker.py

@@ -88,42 +88,100 @@ class TestScenarioConfigChecker:
         Config._compile_configs()
         input_dn_config = DataNodeConfig("input_dn")
         output_dn_config = DataNodeConfig("output_dn")
-        test_dn_config = DataNodeConfig("test")
+        additional_dn_config = DataNodeConfig("additional_dn")
         task_config = TaskConfig("bar", print, [input_dn_config], [output_dn_config])
-        test_task_config = TaskConfig("test", print, [test_dn_config], [output_dn_config])
 
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
-        config._sections[ScenarioConfig.name]["new"]._properties["test"] = "test"
         config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
         Config._collector = IssueCollector()
         Config.check()
         assert len(Config._collector.errors) == 0
 
-        config._sections[ScenarioConfig.name]["new"]._tasks = [test_task_config]
+        config._sections[ScenarioConfig.name]["new"]._properties["bar"] = "bar"
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
+        assert (
+            "The id of the TaskConfig `bar` is overlapping with the property `bar` of ScenarioConfig `new`."
+            in caplog.text
+        )
+
+        config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [additional_dn_config]
+        config._sections[ScenarioConfig.name]["new"]._properties["additional_dn"] = "additional_dn"
         with pytest.raises(SystemExit):
             Config._collector = IssueCollector()
             Config.check()
         assert len(Config._collector.errors) == 2
         assert (
-            "The id of the TaskConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the DataNodeConfig `additional_dn` is overlapping"
+            " with the property `additional_dn` of ScenarioConfig `new`." in caplog.text
+        )
+
+        config._sections[ScenarioConfig.name]["new"].add_sequences({"sq": [task_config]})
+        config._sections[ScenarioConfig.name]["new"]._properties["sq"] = "sq"
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 3
+        assert "The sequence name `sq` is overlapping with the property `sq` of ScenarioConfig `new`." in caplog.text
+
+    def test_check_if_children_ids_are_overlapping(self, caplog):
+        config = Config._applied_config
+        Config._compile_configs()
+        input_dn_config = DataNodeConfig("input_dn")
+        output_dn_config = DataNodeConfig("output_dn")
+        test_dn_config = DataNodeConfig("test")
+        bar_dn_config = DataNodeConfig("bar")
+        test_task_config = TaskConfig("test", print, [test_dn_config], [output_dn_config])
+        bar_task_config = TaskConfig("bar", print, [input_dn_config], [output_dn_config])
+
+        config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
+        config._sections[ScenarioConfig.name]["new"]._tasks = [bar_task_config]
+        Config._collector = IssueCollector()
+        Config.check()
+        assert len(Config._collector.errors) == 0
+
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [bar_dn_config]
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
+        assert (
+            "The id of the TaskConfig `bar` is overlapping with the data node `bar` of ScenarioConfig `new`."
             in caplog.text
         )
+
+        config._sections[ScenarioConfig.name]["new"]._tasks = [test_task_config]
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 1
         assert (
-            "The id of the DataNodeConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the TaskConfig `test` is overlapping with the data node `test` of ScenarioConfig `new`."
             in caplog.text
         )
 
-        config._sections[ScenarioConfig.name]["new"]._tasks = [task_config]
-        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [test_dn_config]
+        config._sections[ScenarioConfig.name]["new"]._tasks = [bar_task_config]
+        config._sections[ScenarioConfig.name]["new"]._additional_data_nodes = [bar_dn_config]
         with pytest.raises(SystemExit):
             Config._collector = IssueCollector()
             Config.check()
         assert len(Config._collector.errors) == 1
         assert (
-            "The id of the DataNodeConfig `test` is overlapping with the property `test` of ScenarioConfig `new`."
+            "The id of the TaskConfig `bar` is overlapping with the data node `bar` of ScenarioConfig `new`."
             in caplog.text
         )
 
+        config._sections[ScenarioConfig.name]["new"].add_sequences({"bar": [bar_task_config]})
+        with pytest.raises(SystemExit):
+            Config._collector = IssueCollector()
+            Config.check()
+        assert len(Config._collector.errors) == 3
+        assert "The sequence name `bar` is overlapping with the data node `bar` of ScenarioConfig `new`." in caplog.text
+        assert "The sequence name `bar` is overlapping with the task `bar` of ScenarioConfig `new`." in caplog.text
+
     def test_check_task_configs(self, caplog):
         Config._collector = IssueCollector()
         config = Config._applied_config
@@ -197,7 +255,7 @@ class TestScenarioConfigChecker:
         assert len(Config._collector.infos) == 0
 
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
-        config._sections[ScenarioConfig.name]["new"]._tasks = [TaskConfig("bar", print)]
+        config._sections[ScenarioConfig.name]["new"]._tasks = [TaskConfig("foo", print)]
         Config._collector = IssueCollector()
         Config.check()
         assert len(Config._collector.errors) == 0
@@ -264,7 +322,7 @@ class TestScenarioConfigChecker:
         output_dn_config = DataNodeConfig("output_dn")
         config._sections[ScenarioConfig.name]["new"] = copy(config._sections[ScenarioConfig.name]["default"])
         config._sections[ScenarioConfig.name]["new"]._tasks = [
-            TaskConfig("bar", print, [input_dn_config], [output_dn_config])
+            TaskConfig("foo", print, [input_dn_config], [output_dn_config])
         ]
         Config._collector = IssueCollector()
         Config.check()

+ 5 - 4
tests/core/cycle/test_cycle.py

@@ -8,6 +8,7 @@
 # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
+
 import datetime
 from datetime import timedelta
 
@@ -49,7 +50,7 @@ def test_create_cycle_entity(current_datetime):
     assert cycle_1.creation_date == current_datetime
     assert cycle_1.start_date == current_datetime
     assert cycle_1.end_date == current_datetime
-    assert cycle_1.key == "value"
+    assert cycle_1.properties["key"] == "value"
     assert cycle_1.frequency == Frequency.DAILY
 
     cycle_2 = Cycle(Frequency.YEARLY, {}, current_datetime, current_datetime, current_datetime)
@@ -111,13 +112,13 @@ def test_add_property_to_scenario(current_datetime):
         name="foo",
     )
     assert cycle.properties == {"key": "value"}
-    assert cycle.key == "value"
+    assert cycle.properties["key"] == "value"
 
     cycle.properties["new_key"] = "new_value"
 
     assert cycle.properties == {"key": "value", "new_key": "new_value"}
-    assert cycle.key == "value"
-    assert cycle.new_key == "new_value"
+    assert cycle.properties["key"] == "value"
+    assert cycle.properties["new_key"] == "new_value"
 
 
 def test_auto_set_and_reload(current_datetime):

+ 1 - 1
tests/core/cycle/test_cycle_manager.py

@@ -91,7 +91,7 @@ def test_create_and_delete_cycle_entity(tmpdir):
     assert cycle_1.start_date is not None
     assert cycle_1.end_date is not None
     assert cycle_1.start_date < cycle_1.creation_date < cycle_1.end_date
-    assert cycle_1.key == "value"
+    assert cycle_1.properties["key"] == "value"
     assert cycle_1.frequency == Frequency.DAILY
 
     cycle_1_id = cycle_1.id

+ 4 - 4
tests/core/data/test_csv_data_node.py

@@ -65,8 +65,8 @@ class TestCSVDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == default_path
-        assert dn.has_header is False
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["has_header"] is False
+        assert dn.properties["exposed_type"] == "pandas"
 
         csv_dn_config = Config.configure_csv_data_node(
             id="foo", default_path=default_path, has_header=True, exposed_type=MyCustomObject
@@ -74,8 +74,8 @@ class TestCSVDataNode:
         dn = _DataManagerFactory._build_manager()._create_and_set(csv_dn_config, None, None)
         assert dn.storage_type() == "csv"
         assert dn.config_id == "foo"
-        assert dn.has_header is True
-        assert dn.exposed_type == MyCustomObject
+        assert dn.properties["has_header"] is True
+        assert dn.properties["exposed_type"] == MyCustomObject
 
         with pytest.raises(InvalidConfigurationId):
             CSVDataNode(

+ 2 - 2
tests/core/data/test_data_manager.py

@@ -303,14 +303,14 @@ class TestDataManager:
         assert csv_dn.config_id == "foo"
         assert isinstance(csv_dn, CSVDataNode)
         assert csv_dn._path == "path_from_config_file"
-        assert csv_dn.has_header
+        assert csv_dn.properties["has_header"]
 
         csv_dn_cfg = Config.configure_data_node(id="baz", storage_type="csv", path="bar", has_header=True)
         csv_dn = _DataManager._create_and_set(csv_dn_cfg, None, None)
         assert csv_dn.config_id == "baz"
         assert isinstance(csv_dn, CSVDataNode)
         assert csv_dn._path == "bar"
-        assert csv_dn.has_header
+        assert csv_dn.properties["has_header"]
 
     def test_get_if_not_exists(self):
         with pytest.raises(ModelNotFound):

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

@@ -670,7 +670,6 @@ class TestDataNode:
             dn = _DataManager._bulk_get_or_create([dn_config])[dn_config]
             assert dn._properties.data["prop"] == "ENV[FOO]"
             assert dn.properties["prop"] == "bar"
-            assert dn.prop == "bar"
 
     def test_path_populated_with_config_default_path(self):
         dn_config = Config.configure_data_node("data_node", "pickle", default_path="foo.p")

+ 18 - 18
tests/core/data/test_excel_data_node.py

@@ -94,17 +94,17 @@ class TestExcelDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == path
-        assert dn.has_header is False
-        assert dn.sheet_name == "Sheet1"
+        assert dn.properties["has_header"] is False
+        assert dn.properties["sheet_name"] == "Sheet1"
 
         excel_dn_config_1 = Config.configure_excel_data_node(
             id="baz", default_path=path, has_header=True, sheet_name="Sheet1", exposed_type=MyCustomObject
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_1, None, None)
         assert isinstance(dn_1, ExcelDataNode)
-        assert dn_1.has_header is True
-        assert dn_1.sheet_name == "Sheet1"
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["has_header"] is True
+        assert dn_1.properties["sheet_name"] == "Sheet1"
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
         excel_dn_config_2 = Config.configure_excel_data_node(
             id="baz",
@@ -115,16 +115,16 @@ class TestExcelDataNode:
         )
         dn_2 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_2, None, None)
         assert isinstance(dn_2, ExcelDataNode)
-        assert dn_2.sheet_name == sheet_names
-        assert dn_2.exposed_type == {"Sheet1": "pandas", "Sheet2": "numpy"}
+        assert dn_2.properties["sheet_name"] == sheet_names
+        assert dn_2.properties["exposed_type"] == {"Sheet1": "pandas", "Sheet2": "numpy"}
 
         excel_dn_config_3 = Config.configure_excel_data_node(
             id="baz", default_path=path, has_header=True, sheet_name=sheet_names, exposed_type=MyCustomObject
         )
         dn_3 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_3, None, None)
         assert isinstance(dn_3, ExcelDataNode)
-        assert dn_3.sheet_name == sheet_names
-        assert dn_3.exposed_type == MyCustomObject
+        assert dn_3.properties["sheet_name"] == sheet_names
+        assert dn_3.properties["exposed_type"] == MyCustomObject
 
         excel_dn_config_4 = Config.configure_excel_data_node(
             id="baz",
@@ -135,8 +135,8 @@ class TestExcelDataNode:
         )
         dn_4 = _DataManagerFactory._build_manager()._create_and_set(excel_dn_config_4, None, None)
         assert isinstance(dn_4, ExcelDataNode)
-        assert dn_4.sheet_name == sheet_names
-        assert dn_4.exposed_type == {"Sheet1": MyCustomObject, "Sheet2": MyCustomObject2}
+        assert dn_4.properties["sheet_name"] == sheet_names
+        assert dn_4.properties["exposed_type"] == {"Sheet1": MyCustomObject, "Sheet2": MyCustomObject2}
 
     def test_get_user_properties(self, excel_file):
         dn_1 = ExcelDataNode("dn_1", Scope.SCENARIO, properties={"path": "data/node/path"})
@@ -204,7 +204,7 @@ class TestExcelDataNode:
             pathlib.Path(__file__).parent.resolve(), "data_sample/example_2.xlsx"
         )  # ["Sheet1", "Sheet2", "Sheet3"]
         dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"default_path": path, "exposed_type": MyCustomObject1})
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         dn.read()
         dn.path = new_path
         dn.read()
@@ -214,7 +214,7 @@ class TestExcelDataNode:
             Scope.SCENARIO,
             properties={"default_path": path, "exposed_type": MyCustomObject1, "sheet_name": ["Sheet4"]},
         )
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         with pytest.raises(NonExistingExcelSheet):
             dn.read()
 
@@ -264,14 +264,14 @@ class TestExcelDataNode:
             "foo", Scope.SCENARIO, properties={"default_path": "notexistyet.xlsx", "exposed_type": MyCustomObject1}
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == MyCustomObject1
+        assert dn.properties["exposed_type"] == MyCustomObject1
         dn = ExcelDataNode(
             "foo",
             Scope.SCENARIO,
             properties={"default_path": "notexistyet.xlsx", "exposed_type": [MyCustomObject1, MyCustomObject2]},
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == [MyCustomObject1, MyCustomObject2]
+        assert dn.properties["exposed_type"] == [MyCustomObject1, MyCustomObject2]
         dn = ExcelDataNode(
             "foo",
             Scope.SCENARIO,
@@ -281,12 +281,12 @@ class TestExcelDataNode:
             },
         )
         assert dn.path == "notexistyet.xlsx"
-        assert dn.exposed_type == {"Sheet1": MyCustomObject1, "Sheet2": MyCustomObject2}
+        assert dn.properties["exposed_type"] == {"Sheet1": MyCustomObject1, "Sheet2": MyCustomObject2}
 
     def test_exposed_type_default(self):
         path = os.path.join(pathlib.Path(__file__).parent.resolve(), "data_sample/example.xlsx")
         dn = ExcelDataNode("foo", Scope.SCENARIO, properties={"default_path": path, "sheet_name": "Sheet1"})
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["exposed_type"] == "pandas"
         data = dn.read()
         assert isinstance(data, pd.DataFrame)
 
@@ -295,7 +295,7 @@ class TestExcelDataNode:
         dn = ExcelDataNode(
             "foo", Scope.SCENARIO, properties={"default_path": path, "exposed_type": "pandas", "sheet_name": "Sheet1"}
         )
-        assert dn.exposed_type == "pandas"
+        assert dn.properties["exposed_type"] == "pandas"
         data = dn.read()
         assert isinstance(data, pd.DataFrame)
 

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

@@ -108,7 +108,6 @@ class TestJSONDataNode:
         assert isinstance(dn_2, JSONDataNode)
         assert dn_2.storage_type() == "json"
         assert dn_2.properties["encoding"] == "utf-16"
-        assert dn_2.encoding == "utf-16"
 
         json_dn_config_3 = Config.configure_json_data_node(
             id="foo", default_path=path, encoder=MyCustomEncoder, decoder=MyCustomDecoder

+ 4 - 4
tests/core/data/test_parquet_data_node.py

@@ -84,16 +84,16 @@ class TestParquetDataNode:
         assert dn.job_ids == []
         assert not dn.is_ready_for_reading
         assert dn.path == path
-        assert dn.exposed_type == "pandas"
-        assert dn.compression == "snappy"
-        assert dn.engine == "pyarrow"
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["compression"] == "snappy"
+        assert dn.properties["engine"] == "pyarrow"
 
         parquet_dn_config_1 = Config.configure_parquet_data_node(
             id="bar", default_path=path, compression=compression, exposed_type=MyCustomObject
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(parquet_dn_config_1, None, None)
         assert isinstance(dn_1, ParquetDataNode)
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
         with pytest.raises(InvalidConfigurationId):
             dn = ParquetDataNode("foo bar", Scope.SCENARIO, properties={"path": path, "name": "super name"})

+ 1 - 1
tests/core/data/test_read_excel_data_node.py

@@ -584,7 +584,7 @@ def test_read_multi_sheet_without_header_single_custom_object_exposed_type():
     )
 
     data_custom = excel_data_node_as_custom_object.read()
-    assert excel_data_node_as_custom_object.exposed_type == MyCustomObject1
+    assert excel_data_node_as_custom_object.properties["exposed_type"] == MyCustomObject1
     assert isinstance(data_custom, Dict)
     assert len(data_custom) == 2
     assert all(len(data_custom[sheet_name]) == 6 for sheet_name in sheet_names)

+ 5 - 5
tests/core/data/test_sql_data_node.py

@@ -119,9 +119,9 @@ class TestSQLDataNode:
         assert dn.owner_id is None
         assert dn.job_ids == []
         assert dn.is_ready_for_reading
-        assert dn.exposed_type == "pandas"
-        assert dn.read_query == "SELECT * FROM example"
-        assert dn.write_query_builder == my_write_query_builder_with_pandas
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["read_query"] == "SELECT * FROM example"
+        assert dn.properties["write_query_builder"] == my_write_query_builder_with_pandas
 
         sql_dn_config_1 = Config.configure_sql_data_node(
             id="foo",
@@ -131,8 +131,8 @@ class TestSQLDataNode:
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(sql_dn_config_1, None, None)
         assert isinstance(dn, SQLDataNode)
-        assert dn_1.exposed_type == MyCustomObject
-        assert dn_1.append_query_builder == my_append_query_builder_with_pandas
+        assert dn_1.properties["exposed_type"] == MyCustomObject
+        assert dn_1.properties["append_query_builder"] == my_append_query_builder_with_pandas
 
     @pytest.mark.parametrize("properties", __sql_properties)
     def test_get_user_properties(self, properties):

+ 3 - 3
tests/core/data/test_sql_table_data_node.py

@@ -97,8 +97,8 @@ class TestSQLTableDataNode:
         assert dn.owner_id is None
         assert dn.job_ids == []
         assert dn.is_ready_for_reading
-        assert dn.exposed_type == "pandas"
-        assert dn.table_name == "example"
+        assert dn.properties["exposed_type"] == "pandas"
+        assert dn.properties["table_name"] == "example"
         assert dn._get_base_read_query() == "SELECT * FROM example"
 
         sql_table_dn_config_1 = Config.configure_sql_table_data_node(
@@ -106,7 +106,7 @@ class TestSQLTableDataNode:
         )
         dn_1 = _DataManagerFactory._build_manager()._create_and_set(sql_table_dn_config_1, None, None)
         assert isinstance(dn_1, SQLTableDataNode)
-        assert dn_1.exposed_type == MyCustomObject
+        assert dn_1.properties["exposed_type"] == MyCustomObject
 
     @pytest.mark.parametrize("properties", __sql_properties)
     def test_get_user_properties(self, properties):

+ 26 - 7
tests/core/scenario/test_scenario.py

@@ -15,14 +15,20 @@ import pytest
 
 from taipy.config import Frequency
 from taipy.config.common.scope import Scope
+from taipy.config.config import Config
 from taipy.config.exceptions.exceptions import InvalidConfigurationId
+from taipy.core import create_scenario
 from taipy.core.common._utils import _Subscriber
 from taipy.core.cycle._cycle_manager_factory import _CycleManagerFactory
 from taipy.core.cycle.cycle import Cycle, CycleId
 from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.in_memory import DataNode, InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
-from taipy.core.exceptions.exceptions import SequenceAlreadyExists, SequenceTaskDoesNotExistInScenario
+from taipy.core.exceptions.exceptions import (
+    AttributeKeyAlreadyExisted,
+    SequenceAlreadyExists,
+    SequenceTaskDoesNotExistInScenario,
+)
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario_id import ScenarioId
@@ -56,7 +62,7 @@ def test_create_primary_scenario(cycle):
     assert scenario.data_nodes == {}
     assert scenario.sequences == {}
     assert scenario.properties == {"key": "value"}
-    assert scenario.key == "value"
+    assert scenario.properties["key"] == "value"
     assert scenario.creation_date is not None
     assert scenario.is_primary
     assert scenario.cycle == cycle
@@ -156,6 +162,18 @@ def test_create_scenario_and_add_sequences():
     assert scenario.sequences == {"sequence_1": scenario.sequence_1, "sequence_2": scenario.sequence_2}
 
 
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    s_cfg = Config.configure_scenario("foo", additional_data_node_configs=[dn_cfg])
+    scenario = create_scenario(s_cfg)
+
+    scenario.key = "value"
+    assert scenario.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        scenario.bar = "KeyAlreadyUsed"
+
+
 def test_create_scenario_overlapping_sequences():
     input_1 = PickleDataNode("input_1", Scope.SCENARIO)
     output_1 = PickleDataNode("output_1", Scope.SCENARIO)
@@ -453,11 +471,11 @@ def test_update_sequence(data_node):
 
     assert len(scenario.sequences) == 1
     assert scenario.sequences["seq_1"].tasks == {"foo": task_1}
-    assert scenario.sequences["seq_1"].name == "seq_1"
+    assert scenario.sequences["seq_1"].properties["name"] == "seq_1"
     scenario.update_sequence("seq_1", [task_2], {"new_key": "new_value"}, [])
     assert len(scenario.sequences) == 1
     assert scenario.sequences["seq_1"].tasks == {"bar": task_2}
-    assert scenario.sequences["seq_1"].name == "seq_1"
+    assert scenario.sequences["seq_1"].properties["name"] == "seq_1"
     assert scenario.sequences["seq_1"].properties["new_key"] == "new_value"
 
 
@@ -465,6 +483,7 @@ def test_add_rename_and_remove_sequences_within_context(data_node):
     task_1 = Task("task_1", {}, print, output=[data_node])
     task_2 = Task("task_2", {}, print, input=[data_node])
     _TaskManagerFactory._build_manager()._set(task_1)
+    _TaskManagerFactory._build_manager()._set(task_2)
     scenario = Scenario(config_id="scenario", tasks={task_1, task_2}, properties={})
     _ScenarioManagerFactory._build_manager()._set(scenario)
 
@@ -490,13 +509,13 @@ def test_add_rename_and_remove_sequences_within_context(data_node):
 def test_add_property_to_scenario():
     scenario = Scenario("foo", set(), {"key": "value"})
     assert scenario.properties == {"key": "value"}
-    assert scenario.key == "value"
+    assert scenario.properties["key"] == "value"
 
     scenario.properties["new_key"] = "new_value"
 
     assert scenario.properties == {"key": "value", "new_key": "new_value"}
-    assert scenario.key == "value"
-    assert scenario.new_key == "new_value"
+    assert scenario.properties["key"] == "value"
+    assert scenario.properties["new_key"] == "new_value"
 
 
 def test_add_cycle_to_scenario(cycle):

+ 22 - 22
tests/core/scenario/test_scenario_manager.py

@@ -1352,17 +1352,17 @@ def test_tags():
     _ScenarioManager._set(scenario_1_tag)
 
     # test getters
-    assert not _ScenarioManager._get_by_tag(cycle_3, "fst")
-    assert not _ScenarioManager._get_by_tag(cycle_3, "scd")
-    assert not _ScenarioManager._get_by_tag(cycle_3, "thd")
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "fst") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "scd") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "thd") == []
 
-    assert _ScenarioManager._get_by_tag(cycle_2, "fst") == scenario_2_tags
-    assert _ScenarioManager._get_by_tag(cycle_2, "scd") == scenario_2_tags
-    assert not _ScenarioManager._get_by_tag(cycle_2, "thd")
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "fst") == [scenario_2_tags]
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "scd") == [scenario_2_tags]
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "thd") == []
 
-    assert _ScenarioManager._get_by_tag(cycle_1, "fst") == scenario_1_tag
-    assert not _ScenarioManager._get_by_tag(cycle_1, "scd")
-    assert not _ScenarioManager._get_by_tag(cycle_1, "thd")
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_1, "fst") == [scenario_1_tag]
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_1, "scd") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_1, "thd") == []
 
     assert len(_ScenarioManager._get_all_by_tag("NOT_EXISTING")) == 0
     assert scenario_1_tag in _ScenarioManager._get_all_by_tag("fst")
@@ -1372,24 +1372,24 @@ def test_tags():
 
     # test tag cycle mgt
 
-    _ScenarioManager._tag(
-        scenario_no_tag, "fst"
-    )  # tag sc_no_tag should untag sc_1_tag with same cycle but not sc_2_tags
+    _ScenarioManager._tag(scenario_no_tag, "fst")  # tag sc_no_tag with fst should not affect sc_1_tag and sc_2_tags
 
-    assert not _ScenarioManager._get_by_tag(cycle_3, "fst")
-    assert not _ScenarioManager._get_by_tag(cycle_3, "scd")
-    assert not _ScenarioManager._get_by_tag(cycle_3, "thd")
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "fst") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "scd") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_3, "thd") == []
 
-    assert _ScenarioManager._get_by_tag(cycle_2, "fst") == scenario_2_tags
-    assert _ScenarioManager._get_by_tag(cycle_2, "scd") == scenario_2_tags
-    assert not _ScenarioManager._get_by_tag(cycle_2, "thd")
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "fst") == [scenario_2_tags]
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "scd") == [scenario_2_tags]
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_2, "thd") == []
 
-    assert _ScenarioManager._get_by_tag(cycle_1, "fst") == scenario_no_tag
-    assert not _ScenarioManager._get_by_tag(cycle_1, "scd")
-    assert not _ScenarioManager._get_by_tag(cycle_1, "thd")
+    assert sorted([s.id for s in _ScenarioManager._get_all_by_cycle_tag(cycle_1, "fst")]) == sorted(
+        [s.id for s in [scenario_no_tag, scenario_1_tag]]
+    )
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_1, "scd") == []
+    assert _ScenarioManager._get_all_by_cycle_tag(cycle_1, "thd") == []
 
     assert len(_ScenarioManager._get_all_by_tag("NOT_EXISTING")) == 0
-    assert len(_ScenarioManager._get_all_by_tag("fst")) == 2
+    assert len(_ScenarioManager._get_all_by_tag("fst")) == 3
     assert scenario_2_tags in _ScenarioManager._get_all_by_tag("fst")
     assert scenario_no_tag in _ScenarioManager._get_all_by_tag("fst")
     assert _ScenarioManager._get_all_by_tag("scd") == [scenario_2_tags]

+ 0 - 0
tests/core/scenario/test_scenario_manager_with_sql_repo.py


+ 23 - 6
tests/core/sequence/test_sequence.py

@@ -20,6 +20,7 @@ from taipy.core.data._data_manager_factory import _DataManagerFactory
 from taipy.core.data.data_node import DataNode
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
+from taipy.core.exceptions import AttributeKeyAlreadyExisted
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.sequence._sequence_manager import _SequenceManager
@@ -38,7 +39,7 @@ def test_sequence_equals():
     sequence_1 = scenario.sequences["print"]
     sequence_id = sequence_1.id
 
-    assert sequence_1.name == "print"
+    assert sequence_1.properties["name"] == "print"
     sequence_2 = _SequenceManager._get(sequence_id)
     # To test if instance is same type
     task = Task("task", {}, print, [], [], sequence_id)
@@ -56,7 +57,7 @@ def test_create_sequence():
     sequence = Sequence({"description": "description"}, [task], sequence_id=SequenceId("name_1"))
     assert sequence.id == "name_1"
     assert sequence.owner_id is None
-    assert sequence.description == "description"
+    assert sequence.properties["description"] == "description"
     assert sequence.foo == input
     assert sequence.bar == output
     assert sequence.baz.id == task.id
@@ -80,7 +81,7 @@ def test_create_sequence():
     )
     assert sequence_1.id == "name_1"
     assert sequence_1.owner_id == "owner_id"
-    assert sequence_1.description == "description"
+    assert sequence_1.properties["description"] == "description"
     assert sequence_1.input == input_1
     assert sequence_1.output == output_1
     assert sequence_1.task_1 == task_1
@@ -109,7 +110,7 @@ def test_create_sequence():
     )
     assert sequence_2.owner_id == "owner_id"
     assert sequence_2.id == "name_2"
-    assert sequence_2.description == "description"
+    assert sequence_2.properties["description"] == "description"
     assert sequence_2.tasks == {task.config_id: task, task_1.config_id: task_1}
     assert sequence_2.data_nodes == {"foo": input, "bar": output, "input": input_1, "output": output_1}
     assert sequence_2.parent_ids == {"parent_id_1", "parent_id_2"}
@@ -122,8 +123,24 @@ def test_create_sequence():
                 return self.label
 
         get_mck.return_value = MockOwner()
-        assert sequence_2.get_label() == "owner_label > " + sequence_2.name
-        assert sequence_2.get_simple_label() == sequence_2.name
+        assert sequence_2.get_label() == "owner_label > " + sequence_2.properties["name"]
+        assert sequence_2.get_simple_label() == sequence_2.properties["name"]
+
+
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    task_config = Config.configure_task("print", print, [dn_cfg], None)
+    scenario_config = Config.configure_scenario("scenario", [task_config])
+
+    scenario = _ScenarioManager._create(scenario_config)
+    scenario.add_sequences({"seq": list(scenario.tasks.values())})
+    sequence = scenario.sequences["seq"]
+
+    sequence.key = "value"
+    assert sequence.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        sequence.bar = "KeyAlreadyUsed"
 
 
 def test_check_consistency():

+ 1 - 1
tests/core/sequence/test_sequence_manager.py

@@ -415,7 +415,7 @@ def test_get_or_create_data():
     scenario.add_sequences({"by_6": list(scenario.tasks.values())})
     sequence = scenario.sequences["by_6"]
 
-    assert sequence.name == "by_6"
+    assert sequence.properties["name"] == "by_6"
 
     assert len(_DataManager._get_all()) == 3
     assert len(_TaskManager._get_all()) == 2

+ 17 - 1
tests/core/task/test_task.py

@@ -21,6 +21,8 @@ from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.csv import CSVDataNode
 from taipy.core.data.data_node import DataNode
 from taipy.core.data.in_memory import InMemoryDataNode
+from taipy.core.exceptions import AttributeKeyAlreadyExisted
+from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.task._task_manager import _TaskManager
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
 from taipy.core.task.task import Task
@@ -94,7 +96,7 @@ def test_create_task():
     assert task.owner_id == "owner_id"
     assert task.parent_ids == {"parent_id_1", "parent_id_2"}
     assert task.name_1ea == abc_dn
-    assert task.name_1ea.path == path
+    assert task.name_1ea.properties["path"] == path
     with pytest.raises(AttributeError):
         _ = task.bar
     with mock.patch("taipy.core.get") as get_mck:
@@ -110,6 +112,20 @@ def test_create_task():
         assert task.get_simple_label() == task.config_id
 
 
+def test_get_set_attribute():
+    dn_cfg = Config.configure_data_node("bar")
+    task_config = Config.configure_task("print", print, [dn_cfg], None)
+    scenario_config = Config.configure_scenario("scenario", [task_config])
+    scenario = _ScenarioManager._create(scenario_config)
+    task = scenario.tasks["print"]
+
+    task.key = "value"
+    assert task.key == "value"
+
+    with pytest.raises(AttributeKeyAlreadyExisted):
+        task.bar = "KeyAlreadyUsed"
+
+
 def test_can_not_change_task_output(output):
     task = Task("name_1", {}, print, output=output)
 

+ 8 - 0
tests/gui/control/test_button.py

@@ -27,6 +27,14 @@ def test_button_md_2(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_button_md_width(gui: Gui, test_client, helpers):
+    gui._bind_var_val("name", "World!")
+    gui._bind_var_val("btn_id", "button1")
+    md_string = "<|Hello {name}|button|id={btn_id}|width=70%|>"
+    expected_list = ["<Button", 'defaultLabel="Hello World!"', "label={tp_TpExPr_Hello_name_TPMDL_0_0", 'width="70%"']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_button_html_1(gui: Gui, test_client, helpers):
     gui._bind_var_val("name", "World!")
     gui._bind_var_val("btn_id", "button1")

+ 13 - 0
tests/gui/control/test_date.py

@@ -40,6 +40,19 @@ def test_date_md_2(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_date_md_width(gui: Gui, test_client, helpers):
+    gui._bind_var_val("date", datetime.strptime("15 Dec 2020", "%d %b %Y"))
+    md_string = "<|{date}|date|width=70%|>"
+    expected_list = [
+        "<DateSelector",
+        'defaultDate="2020-12-',
+        'updateVarName="_TpDt_tpec_TpExPr_date_TPMDL_0"',
+        'width="70%"',
+        "date={_TpDt_tpec_TpExPr_date_TPMDL_0}",
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_date_html_1(gui: Gui, test_client, helpers):
     gui._bind_var_val("date", datetime.strptime("15 Dec 2020", "%d %b %Y"))
     html_string = '<taipy:date date="{date}" />'

+ 15 - 0
tests/gui/control/test_date_range.py

@@ -45,6 +45,21 @@ def test_date_range_md_2(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_date_range_md_width(gui: Gui, helpers):
+    gui._bind_var_val(
+        "dates", [datetime.strptime("15 Dec 2020", "%d %b %Y"), datetime.strptime("31 Dec 2020", "%d %b %Y")]
+    )
+    md_string = "<|{dates}|date_range|width=70%|>"
+    expected_list = [
+        "<DateRange",
+        'defaultDates="[&quot;2020-12-',
+        'updateVarName="_TpDr_tpec_TpExPr_dates_TPMDL_0"',
+        'width="70%"',
+        "dates={_TpDr_tpec_TpExPr_dates_TPMDL_0}",
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_date_range_html_1(gui: Gui, test_client, helpers):
     gui._bind_var_val(
         "dates", [datetime.strptime("15 Dec 2020", "%d %b %Y"), datetime.strptime("31 Dec 2020", "%d %b %Y")]

+ 12 - 0
tests/gui/control/test_file_download.py

@@ -64,6 +64,18 @@ def test_file_download_any_file_md(gui: Gui, test_client, helpers):
         helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_file_download_url_width_md(gui: Gui, test_client, helpers):
+    gui._bind_var_val("content", "some_url")
+    md_string = "<|{content}|file_download|width=70%|>"
+    expected_list = [
+        "<FileDownload",
+        "content={_TpC_tpec_TpExPr_content_TPMDL_0}",
+        'defaultContent="some_url"',
+        'width="70%"',
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_file_download_url_html(gui: Gui, test_client, helpers):
     gui._bind_var_val("content", "some_url")
     html_string = '<taipy:file_download content="{content}" />'

+ 13 - 0
tests/gui/control/test_file_selector.py

@@ -24,6 +24,19 @@ def test_file_selector_md(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_file_selector_width_md(gui: Gui, test_client, helpers):
+    gui._bind_var_val("content", None)
+    md_string = "<|{content}|file_selector|label=label|on_action=action|width=70%|>"
+    expected_list = [
+        "<FileSelector",
+        'updateVarName="tpec_TpExPr_content_TPMDL_0"',
+        'label="label"',
+        'onAction="action"',
+        'width="70%"',
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_file_selector_html(gui: Gui, test_client, helpers):
     gui._bind_var_val("content", None)
     html_string = '<taipy:file_selector content="{content}" label="label" on_action="action" />'

+ 17 - 1
tests/gui/control/test_input.py

@@ -28,6 +28,21 @@ def test_input_md(gui: Gui, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_input_md_width(gui: Gui, helpers):
+    x = "Hello World!"  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    md_string = "<|{x}|input|width=70%|>"
+    expected_list = [
+        "<Input",
+        'updateVarName="tpec_TpExPr_x_TPMDL_0"',
+        'defaultValue="Hello World!"',
+        'type="text"',
+        'width="70%"',
+        "value={tpec_TpExPr_x_TPMDL_0}",
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_password_md(gui: Gui, helpers):
     x = "Hello World!"  # noqa: F841
     gui._set_frame(inspect.currentframe())
@@ -59,12 +74,13 @@ def test_input_html_1(gui: Gui, helpers):
 def test_password_html(gui: Gui, helpers):
     x = "Hello World!"  # noqa: F841
     gui._set_frame(inspect.currentframe())
-    html_string = '<taipy:input value="{x}" password="True" />'
+    html_string = '<taipy:input value="{x}" password="True" width="{100}" />'
     expected_list = [
         "<Input",
         'updateVarName="tpec_TpExPr_x_TPMDL_0"',
         'defaultValue="Hello World!"',
         'type="password"',
+        'width={100}',
         "value={tpec_TpExPr_x_TPMDL_0}",
     ]
     helpers.test_control_html(gui, html_string, expected_list)

+ 6 - 0
tests/gui/control/test_number.py

@@ -31,6 +31,12 @@ def test_number_md_2(gui: Gui, test_client, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_number_md_width(gui: Gui, helpers):
+    md_string = "<|10|number|width=70%|>"
+    expected_list = ["<Input", 'value="10"', 'type="number"', 'width="70%"']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_number_html_1(gui: Gui, test_client, helpers):
     gui._bind_var_val("x", 10)
     html_string = '<taipy:number value="{x}" />'

+ 7 - 0
tests/gui/control/test_text.py

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

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

@@ -18,6 +18,12 @@ def test_toggle_md(gui: Gui, helpers):
     helpers.test_control_md(gui, md_string, expected_list)
 
 
+def test_toggle_width_md(gui: Gui, helpers):
+    md_string = "<|toggle|theme|width=70%|>"
+    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""', 'width="70%"']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+
 def test_toggle_allow_unselected_md(gui: Gui, helpers):
     md_string = "<|toggle|lov=1;2|allow_unselect|>"
     expected_list = ["<Toggle", 'unselectedValue=""', "allowUnselect={true}"]

+ 2 - 2
tests/gui_core/test_context_is_deletable.py

@@ -91,7 +91,7 @@ class TestGuiCoreContext_is_deletable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not deletable.")
+                assert "is not deletable" in str(assign.call_args.args[1])
 
     def test_act_on_jobs(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
@@ -127,4 +127,4 @@ class TestGuiCoreContext_is_deletable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in str(assign.call_args.args[1])

+ 8 - 8
tests/gui_core/test_context_is_editable.py

@@ -88,7 +88,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in str(assign.call_args.args[1])
 
     def test_edit_entity(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -122,7 +122,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in str(assign.call_args.args[1])
 
     def test_act_on_jobs(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
@@ -142,7 +142,7 @@ class TestGuiCoreContext_is_editable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not editable.") == -1
+            assert "is not editable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_editable_false):
@@ -158,7 +158,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_data_node(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -192,7 +192,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_lock_datanode_for_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -228,7 +228,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_update_data(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -264,7 +264,7 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]
 
     def test_tabular_data_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -299,4 +299,4 @@ class TestGuiCoreContext_is_editable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not editable.")
+                assert "is not editable" in assign.call_args.args[1]

+ 2 - 2
tests/gui_core/test_context_is_promotable.py

@@ -65,7 +65,7 @@ class TestGuiCoreContext_is_promotable:
             )
             assign.assert_called_once()
             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 "to primary because it doesn't belong to a cycle" in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_promotable", side_effect=mock_is_promotable_false):
@@ -81,4 +81,4 @@ class TestGuiCoreContext_is_promotable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not promotable.")
+                assert "is not promotable" in assign.call_args.args[1]

+ 10 - 10
tests/gui_core/test_context_is_readable.py

@@ -138,7 +138,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_entity(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -172,7 +172,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_submission_status_callback(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
@@ -239,7 +239,7 @@ class TestGuiCoreContext_is_readable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not readable.") == -1
+            assert "is not readable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             gui_core_context.act_on_jobs(
@@ -254,7 +254,7 @@ class TestGuiCoreContext_is_readable:
             )
             assign.assert_called_once()
             assert assign.call_args.args[0] == "error_var"
-            assert str(assign.call_args.args[1]).find("is not readable.") == -1
+            assert "is not readable" not in assign.call_args.args[1]
             assign.reset_mock()
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
@@ -270,7 +270,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
                 assign.reset_mock()
 
                 gui_core_context.act_on_jobs(
@@ -285,7 +285,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_edit_data_node(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -319,7 +319,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_lock_datanode_for_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -355,7 +355,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_get_scenarios_for_owner(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
@@ -402,7 +402,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_tabular_data_edit(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
@@ -437,7 +437,7 @@ class TestGuiCoreContext_is_readable:
                 )
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not readable.")
+                assert "is not readable" in assign.call_args.args[1]
 
     def test_get_data_node_tabular_data(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:

+ 1 - 1
tools/packages/pipfiles/Pipfile3.10.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

+ 1 - 1
tools/packages/pipfiles/Pipfile3.11.max

@@ -72,7 +72,7 @@ version = "==4.2.13"
 "kthread" = {version="==0.2.3"}
 "gitignore-parser" = {version="==0.1.11"}
 "simple-websocket" = {version="==1.0.0"}
-"twisted" = {version="==24.3.0"}
+"twisted" = {version="==24.7.0"}
 "deepdiff" = {version="==7.0.1"}
 "flask-restful" = {version="==0.3.10"}
 "passlib" = {version="==1.7.4"}

Някои файлове не бяха показани, защото твърде много файлове са промени