Просмотр исходного кода

Merge branch 'develop' into feature/#1196-datanode-download-upload-api

trgiangdo 10 месяцев назад
Родитель
Сommit
fcf632fbd1
84 измененных файлов с 3042 добавлено и 942 удалено
  1. 11 2
      .github/workflows/build-and-release-single-package.yml
  2. 3 0
      .github/workflows/build-and-release.yml
  3. 1 1
      .github/workflows/codeql-analysis.yml
  4. 1 1
      .github/workflows/codespell.yml
  5. 6 0
      .github/workflows/frontend.yml
  6. 6 0
      .github/workflows/manage-stale-issue-pr.yml
  7. 6 0
      .github/workflows/overall-tests.yml
  8. 9 3
      .github/workflows/packaging.yml
  9. 1 1
      .github/workflows/publish-single-package.yml
  10. 4 4
      .github/workflows/publish.yml
  11. 2 2
      README.md
  12. 9 15
      doc/gui/examples/broadcast.py
  13. 57 0
      doc/gui/examples/broadcast_callback.py
  14. 48 0
      doc/gui/examples/broadcast_change.py
  15. 9 9
      doc/gui/examples/charts/advanced-python-lib.py
  16. 16 3
      doc/gui/examples/charts/heatmap-drawing-on-top.py
  17. 28 0
      doc/gui/examples/controls/date-min-max.py
  18. 1 7
      doc/gui/examples/controls/metric-color-map.py
  19. 0 1
      doc/gui/examples/controls/metric-hide-value.py
  20. 0 1
      doc/gui/examples/controls/metric-layout.py
  21. 0 1
      doc/gui/examples/controls/metric-range.py
  22. 0 1
      doc/gui/examples/controls/metric-type.py
  23. 0 1
      doc/gui/examples/controls/metric-value-format.py
  24. 25 0
      doc/gui/examples/controls/number-min-max.py
  25. 25 0
      doc/gui/examples/controls/number-step.py
  26. 38 8
      frontend/taipy-gui/base/src/app.ts
  27. 0 2
      frontend/taipy-gui/base/src/exports.ts
  28. 28 30
      frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts
  29. 2 2
      frontend/taipy-gui/base/src/socket.ts
  30. 7 3
      frontend/taipy-gui/base/src/wsAdapter.ts
  31. 254 233
      frontend/taipy-gui/package-lock.json
  32. 7 7
      frontend/taipy-gui/package.json
  33. 1 27
      frontend/taipy-gui/src/components/Taipy/DateRange.tsx
  34. 33 9
      frontend/taipy-gui/src/components/Taipy/DateSelector.tsx
  35. 74 11
      frontend/taipy-gui/src/components/Taipy/Input.spec.tsx
  36. 186 5
      frontend/taipy-gui/src/components/Taipy/Input.tsx
  37. 43 34
      frontend/taipy-gui/src/components/Taipy/Login.tsx
  38. 1 1
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
  39. 34 0
      frontend/taipy-gui/src/components/Taipy/utils.ts
  40. 2 12
      frontend/taipy-gui/src/utils/index.ts
  41. 150 134
      frontend/taipy/package-lock.json
  42. 1 1
      frontend/taipy/package.json
  43. 26 11
      frontend/taipy/src/ScenarioDag.tsx
  44. 6 6
      taipy/core/_entity/_ready_to_run_property.py
  45. 6 7
      taipy/core/_entity/submittable.py
  46. 6 7
      taipy/core/data/_data_manager.py
  47. 6 5
      taipy/core/data/data_node.py
  48. 9 1
      taipy/core/reason/__init__.py
  49. 0 37
      taipy/core/reason/_reason_factory.py
  50. 106 22
      taipy/core/reason/reason.py
  51. 60 0
      taipy/core/reason/reason_collection.py
  52. 40 14
      taipy/core/scenario/_scenario_manager.py
  53. 6 3
      taipy/core/scenario/scenario.py
  54. 5 6
      taipy/core/sequence/_sequence_manager.py
  55. 17 5
      taipy/core/taipy.py
  56. 8 13
      taipy/core/task/_task_manager.py
  57. 14 3
      taipy/gui/_renderers/builder.py
  58. 6 0
      taipy/gui/_renderers/factory.py
  59. 0 1
      taipy/gui/builder/_api_generator.py
  60. 69 6
      taipy/gui/builder/_element.py
  61. 65 0
      taipy/gui/builder/_utils.py
  62. 129 45
      taipy/gui/gui.py
  63. 14 18
      taipy/gui/gui_actions.py
  64. 1 1
      taipy/gui/server.py
  65. 12 5
      taipy/gui/types.py
  66. 731 0
      taipy/gui/utils/unparse.py
  67. 97 59
      taipy/gui/viselements.json
  68. 2 1
      taipy/gui_core/_context.py
  69. 18 18
      tests/core/_entity/test_ready_to_run_property.py
  70. 45 43
      tests/core/common/test_reason.py
  71. 8 2
      tests/core/data/test_data_manager.py
  72. 59 2
      tests/core/scenario/test_scenario_manager.py
  73. 53 0
      tests/core/scenario/test_scenario_manager_with_sql_repo.py
  74. 6 0
      tests/core/test_taipy.py
  75. 46 5
      tests/gui/actions/test_invoke_callback.py
  76. 10 0
      tests/gui/builder/control/test_dialog.py
  77. 33 0
      tests/gui/builder/test_lambda.py
  78. 30 0
      tests/gui/builder/test_on_action.py
  79. 138 0
      tests/gui/gui_specific/test_broadcast.py
  80. 0 12
      tests/gui/gui_specific/test_gui.py
  81. 6 2
      tests/gui/gui_specific/test_shared.py
  82. 6 6
      tests/gui_core/test_context_is_submitable.py
  83. 12 4
      tools/gui/generate_pyi.py
  84. 2 0
      tools/release/fetch_latest_versions.py

+ 11 - 2
.github/workflows/build-and-release-single-package.yml

@@ -18,6 +18,9 @@ on:
         description: "The package to be released (gui, config, core, rest, templates, taipy)"
         description: "The package to be released (gui, config, core, rest, templates, taipy)"
         required: true
         required: true
 
 
+env:
+  NODE_OPTIONS: --max-old-space-size=4096
+
 jobs:
 jobs:
   fetch-versions:
   fetch-versions:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -27,8 +30,7 @@ jobs:
         gui_VERSION: ${{ steps.version-setup.outputs.gui_VERSION }}
         gui_VERSION: ${{ steps.version-setup.outputs.gui_VERSION }}
         rest_VERSION: ${{ steps.version-setup.outputs.rest_VERSION }}
         rest_VERSION: ${{ steps.version-setup.outputs.rest_VERSION }}
         templates_VERSION: ${{ steps.version-setup.outputs.templates_VERSION }}
         templates_VERSION: ${{ steps.version-setup.outputs.templates_VERSION }}
-        VERSION: ${{ steps.version-setup.outputs.VERSION }}
-        NEW_VERSION: ${{ steps.version-setup.outputs.NEW_VERSION }}
+        taipy_VERSION: ${{ steps.version-setup.outputs.taipy_VERSION }}
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
       - name: Extract branch name
       - name: Extract branch name
@@ -165,3 +167,10 @@ jobs:
         shell: bash
         shell: bash
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Ensure Taipy release is marked as latest
+        run: |
+           gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
+        shell: bash
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 3 - 0
.github/workflows/build-and-release.yml

@@ -15,6 +15,9 @@ on:
         description: "The version of the package to be released"
         description: "The version of the package to be released"
         required: true
         required: true
 
 
+env:
+  NODE_OPTIONS: --max-old-space-size=4096
+
 jobs:
 jobs:
   fetch-versions:
   fetch-versions:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 1 - 1
.github/workflows/codeql-analysis.yml

@@ -24,7 +24,7 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v2
       uses: github/codeql-action/init@v2

+ 1 - 1
.github/workflows/codespell.yml

@@ -17,6 +17,6 @@ jobs:
 
 
     steps:
     steps:
       - name: Checkout
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Codespell
       - name: Codespell
         uses: codespell-project/actions-codespell@v2
         uses: codespell-project/actions-codespell@v2

+ 6 - 0
.github/workflows/frontend.yml

@@ -9,6 +9,12 @@ on:
         - 'frontend/taipy-gui/**'
         - 'frontend/taipy-gui/**'
   workflow_dispatch:
   workflow_dispatch:
 
 
+permissions:
+  pull-requests: write
+
+env:
+  NODE_OPTIONS: --max-old-space-size=4096
+
 jobs:
 jobs:
   frontend-jest:
   frontend-jest:
     timeout-minutes: 20
     timeout-minutes: 20

+ 6 - 0
.github/workflows/manage-stale-issue-pr.yml

@@ -4,11 +4,15 @@ on:
   schedule:
   schedule:
     # Run once every day at 9 AM UTC
     # Run once every day at 9 AM UTC
     - cron: 00 9 * * *
     - cron: 00 9 * * *
+  workflow_dispatch:
 
 
 jobs:
 jobs:
   stale-issues-and-prs:
   stale-issues-and-prs:
     name: Comment on possible stable issues and PRs, and close stale PRs
     name: Comment on possible stable issues and PRs, and close stale PRs
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    permissions:
+      issues: write
+      pull-requests: write
     steps:
     steps:
       - uses: actions/stale@v9
       - uses: actions/stale@v9
         with:
         with:
@@ -30,6 +34,8 @@ jobs:
   unassign-issues-labeled-waiting-for-contributor-after-14-days-of-inactivity:
   unassign-issues-labeled-waiting-for-contributor-after-14-days-of-inactivity:
     name: Unassign issues labeled \"🥶Waiting for contributor\" after 14 days of inactivity.
     name: Unassign issues labeled \"🥶Waiting for contributor\" after 14 days of inactivity.
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    permissions:
+      issues: write
     steps:
     steps:
       - uses: boundfoxstudios/action-unassign-contributor-after-days-of-inactivity@v1
       - uses: boundfoxstudios/action-unassign-contributor-after-days-of-inactivity@v1
         with:
         with:

+ 6 - 0
.github/workflows/overall-tests.yml

@@ -7,6 +7,12 @@ on:
     branches: [ develop, dev/*, release/* ]
     branches: [ develop, dev/*, release/* ]
   workflow_dispatch:
   workflow_dispatch:
 
 
+permissions:
+  pull-requests: write
+
+env:
+  NODE_OPTIONS: --max-old-space-size=4096
+
 jobs:
 jobs:
   partial-tests:
   partial-tests:
     uses: ./.github/workflows/partial-tests.yml
     uses: ./.github/workflows/partial-tests.yml

+ 9 - 3
.github/workflows/packaging.yml

@@ -12,13 +12,16 @@ on:
         required: false
         required: false
         default: ""
         default: ""
 
 
+env:
+  NODE_OPTIONS: --max-old-space-size=4096
+
 jobs:
 jobs:
   standard-packages:
   standard-packages:
     timeout-minutes: 30
     timeout-minutes: 30
     strategy:
     strategy:
       matrix:
       matrix:
-        python-versions: [ '3.8', '3.9', '3.10', '3.11', '3.12']
-        os: [ubuntu-latest, macos-13] #, windows-latest]
+        python-versions: [ '3.8', '3.9', '3.10', '3.11', '3.12' ]
+        os: [ubuntu-latest, macos-13, windows-latest]
 
 
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
 
 
@@ -35,7 +38,10 @@ jobs:
       - name: Install Taipy without dependencies
       - name: Install Taipy without dependencies
         run: |
         run: |
           pip install .
           pip install .
-          rm -rf taipy
+
+      - name: Remove local folder
+        run: rm -r taipy
+
       - name: Check Taipy Installation
       - name: Check Taipy Installation
         run: |
         run: |
           python tools/validate_taipy_install.py
           python tools/validate_taipy_install.py

+ 1 - 1
.github/workflows/publish-single-package.yml

@@ -19,7 +19,7 @@ jobs:
     environment: publish
     environment: publish
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
         with:
           sparse-checkout: taipy/${{ matrix.package }}
           sparse-checkout: taipy/${{ matrix.package }}
           sparse-checkout-cone-mode: false
           sparse-checkout-cone-mode: false

+ 4 - 4
.github/workflows/publish.yml

@@ -12,7 +12,7 @@ jobs:
     timeout-minutes: 20
     timeout-minutes: 20
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
       - uses: actions/setup-python@v5
         with:
         with:
           python-version: 3.8
           python-version: 3.8
@@ -43,7 +43,7 @@ jobs:
     environment: publish
     environment: publish
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
         with:
           sparse-checkout: taipy/${{ matrix.package }}
           sparse-checkout: taipy/${{ matrix.package }}
           sparse-checkout-cone-mode: false
           sparse-checkout-cone-mode: false
@@ -76,7 +76,7 @@ jobs:
     environment: publish
     environment: publish
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
       - name: Checks if package is already on on Pypi
       - name: Checks if package is already on on Pypi
         id: check-version
         id: check-version
@@ -108,7 +108,7 @@ jobs:
         os: [ubuntu-latest,windows-latest,macos-13]
         os: [ubuntu-latest,windows-latest,macos-13]
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
       - uses: actions/setup-python@v5
         with:
         with:
           python-version: ${{ matrix.python-versions }}
           python-version: ${{ matrix.python-versions }}

+ 2 - 2
README.md

@@ -1,5 +1,5 @@
-[![Taipy Designer](https://github.com/nevo-david/taipy/assets/100117126/e787ba7b-ec7a-4d3f-a7e4-0f195daadce7)
-](https://taipy.io/enterprise)
+[![Taipy Designer banner](https://github.com/Avaiga/taipy/assets/31435778/6378ffd4-438a-498f-9385-10394f7d53fb)](https://links.taipy.io/306TwUH)
+
 
 
 <div align="center">
 <div align="center">
   <a href="https://taipy.io?utm_source=github" target="_blank">
   <a href="https://taipy.io?utm_source=github" target="_blank">

+ 9 - 15
doc/gui/examples/broadcast.py

@@ -15,14 +15,15 @@
 # -----------------------------------------------------------------------------------------
 # -----------------------------------------------------------------------------------------
 # Demonstrate how to share variable values across multiple clients.
 # Demonstrate how to share variable values across multiple clients.
 # This application creates a thread that increments a value every few seconds.
 # This application creates a thread that increments a value every few seconds.
-# The value is updated for every client using the state.broadcast() method.
-# The text of the button that starts or stops the thread is updated on every client's browser
-# using a direct assignment of the state property because the variable is declared 'shared'.
+# The value is updated for every client in a function invoked by Gui.broadcast_change().
+# The text of the button that starts or stops the thread is updated using the
+# State.assign() method, and udateds on every client's browser because the variable was
+# declared 'shared' by calling Gui.add_shared_variable().
 # -----------------------------------------------------------------------------------------
 # -----------------------------------------------------------------------------------------
 from threading import Event, Thread
 from threading import Event, Thread
 from time import sleep
 from time import sleep
 
 
-from taipy.gui import Gui, State, broadcast_callback
+from taipy.gui import Gui, State
 
 
 counter = 0
 counter = 0
 
 
@@ -35,22 +36,16 @@ button_texts = ["Start", "Stop"]
 button_text = button_texts[0]
 button_text = button_texts[0]
 
 
 
 
-# Invoked by the timer
-def update_counter(state: State, c):
-    # Update all clients
-    state.broadcast("counter", c)
-
-
 def count(event, gui):
 def count(event, gui):
     while not event.is_set():
     while not event.is_set():
         global counter
         global counter
         counter = counter + 1
         counter = counter + 1
-        broadcast_callback(gui, update_counter, [counter])
+        gui.broadcast_change("counter", counter)
         sleep(2)
         sleep(2)
 
 
 
 
 # Start or stop the timer when the button is pressed
 # Start or stop the timer when the button is pressed
-def start_or_stop(state):
+def start_or_stop(state: State):
     global thread
     global thread
     if thread:  # Timer is running
     if thread:  # Timer is running
         thread_event.set()
         thread_event.set()
@@ -59,9 +54,8 @@ def start_or_stop(state):
         thread_event.clear()
         thread_event.clear()
         thread = Thread(target=count, args=[thread_event, state.get_gui()])
         thread = Thread(target=count, args=[thread_event, state.get_gui()])
         thread.start()
         thread.start()
-    # Update button status.
-    # Because "button_text" is shared, all clients are updated
-    state.button_text = button_texts[1 if thread else 0]
+    # Update button status for all states.
+    state.assign("button_text", button_texts[1 if thread else 0])
 
 
 
 
 page = """# Broadcasting values
 page = """# Broadcasting values

+ 57 - 0
doc/gui/examples/broadcast_callback.py

@@ -0,0 +1,57 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Demonstrate how to update the value of a variable across multiple clients.
+# This application creates a thread that sets a variable to the current time.
+# The value is updated for every client when Gui.broadcast_change() is invoked.
+# -----------------------------------------------------------------------------------------
+from datetime import datetime
+from threading import Thread
+from time import sleep
+
+from taipy.gui import Gui
+
+current_time = datetime.now()
+update = False
+
+
+# Update the 'current_time' state variable if 'update' is True
+def update_state(state, updated_time):
+    if state.update:
+        state.current_time = updated_time
+
+
+# The function that executes in its own thread.
+# Call 'update_state()` every second.
+def update_time(gui):
+    while True:
+        gui.broadcast_callback(update_state, [datetime.now()])
+        sleep(1)
+
+
+page = """
+Current time is: <|{current_time}|format=HH:mm:ss|>
+
+Update: <|{update}|toggle|>
+"""
+
+gui = Gui(page)
+
+# Run thread that regularly updates the current time
+thread = Thread(target=update_time, args=[gui], name="clock")
+thread.daemon = True
+thread.start()
+
+gui.run()

+ 48 - 0
doc/gui/examples/broadcast_change.py

@@ -0,0 +1,48 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Demonstrate how to invoke a callback for different clients.
+# This application creates a thread that, every second, invokes a callback for every client
+# so the current time may be updated, under a state-dependant condition.
+# -----------------------------------------------------------------------------------------
+from datetime import datetime
+from threading import Thread
+from time import sleep
+
+from taipy.gui import Gui
+
+current_time = datetime.now()
+
+
+# The function that executes in its own thread.
+# Update the current time every second.
+def update_time(gui):
+    while True:
+        gui.broadcast_change("current_time", datetime.now())
+        sleep(1)
+
+
+page = """
+Current time is: <|{current_time}|format=HH:mm:ss|>
+"""
+
+gui = Gui(page)
+
+# Run thread that regularly updates the current time
+thread = Thread(target=update_time, args=[gui], name="clock")
+thread.daemon = True
+thread.start()
+
+gui.run()

+ 9 - 9
doc/gui/examples/charts/advanced-python-lib.py

@@ -25,19 +25,19 @@ from taipy.gui import Gui
 figure = go.Figure()
 figure = go.Figure()
 
 
 # Add trace for Normal Distribution
 # Add trace for Normal Distribution
-figure.add_trace(go.Violin(name="Normal",
-                           y=np.random.normal(loc=0, scale=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Normal", y=np.random.normal(loc=0, scale=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 
 # Add trace for Exponential Distribution
 # Add trace for Exponential Distribution
-figure.add_trace(go.Violin(name="Exponential",
-                           y=np.random.exponential(scale=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Exponential", y=np.random.exponential(scale=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 
 # Add trace for Uniform Distribution
 # Add trace for Uniform Distribution
-figure.add_trace(go.Violin(name="Uniform",
-                           y=np.random.uniform(low=0, high=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Uniform", y=np.random.uniform(low=0, high=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 
 # Updating layout for better visualization
 # Updating layout for better visualization
 figure.update_layout(title="Different Probability Distributions")
 figure.update_layout(title="Different Probability Distributions")

+ 16 - 3
doc/gui/examples/charts/heatmap-drawing-on-top.py

@@ -31,8 +31,14 @@ def spiral(th):
 
 
 # Prepare the heatmap x and y cell sizes along the axes
 # Prepare the heatmap x and y cell sizes along the axes
 golden_ratio = (1 + numpy.sqrt(5)) / 2.0  # Golden ratio
 golden_ratio = (1 + numpy.sqrt(5)) / 2.0  # Golden ratio
-grid_x = [0, 1, 1 + (1 / (golden_ratio ** 4)), 1 + (1 / (golden_ratio ** 3)), golden_ratio]
-grid_y = [0, 1 / (golden_ratio ** 3), 1 / golden_ratio ** 3 + 1 / golden_ratio ** 4, 1 / (golden_ratio ** 2), 1]
+grid_x = [0, 1, 1 + (1 / (golden_ratio**4)), 1 + (1 / (golden_ratio**3)), golden_ratio]
+grid_y = [
+    0,
+    1 / (golden_ratio**3),
+    1 / golden_ratio**3 + 1 / golden_ratio**4,
+    1 / (golden_ratio**2),
+    1,
+]
 
 
 # Main value is based on the Fibonacci sequence
 # Main value is based on the Fibonacci sequence
 z = [[13, 3, 3, 5], [13, 2, 1, 5], [13, 10, 11, 12], [13, 8, 8, 8]]
 z = [[13, 3, 3, 5], [13, 2, 1, 5], [13, 10, 11, 12], [13, 8, 8, 8]]
@@ -50,7 +56,14 @@ data = [
 ]
 ]
 
 
 # Axis template: hide all ticks, lines and labels
 # Axis template: hide all ticks, lines and labels
-axis = {"range": [0, 2.0], "showgrid": False, "zeroline": False, "showticklabels": False, "ticks": "", "title": ""}
+axis = {
+    "range": [0, 2.0],
+    "showgrid": False,
+    "zeroline": False,
+    "showticklabels": False,
+    "ticks": "",
+    "title": "",
+}
 
 
 layout = {
 layout = {
     # Use the axis template for both x and y axes
     # Use the axis template for both x and y axes

+ 28 - 0
doc/gui/examples/controls/date-min-max.py

@@ -0,0 +1,28 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+import datetime
+
+from taipy.gui import Gui
+
+date = datetime.date(2024, 6, 15)
+start = datetime.date(2024, 5, 15)
+end = datetime.date(2024, 7, 15)
+
+page = """
+<|{date}|date|min={start}|max={end}|>
+"""
+
+Gui(page).run()

+ 1 - 7
doc/gui/examples/controls/metric-color-map.py

@@ -28,16 +28,10 @@ from taipy.gui import Gui
 # }
 # }
 
 
 value = 50
 value = 50
-color_map = {
-    20: "red",
-    40: None,
-    60: "blue",
-    80: None
-}
+color_map = {20: "red", 40: None, 60: "blue", 80: None}
 
 
 page = """
 page = """
 <|{value}|metric|color_map={color_map}|>
 <|{value}|metric|color_map={color_map}|>
 """
 """
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-hide-value.py

@@ -21,4 +21,3 @@ page = """
 
 
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-layout.py

@@ -40,4 +40,3 @@ page = """
 """
 """
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-range.py

@@ -25,4 +25,3 @@ page = """
 
 
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-type.py

@@ -23,4 +23,3 @@ page = """
 """
 """
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-value-format.py

@@ -27,4 +27,3 @@ page = """
 
 
 
 
 Gui(page).run()
 Gui(page).run()
-

+ 25 - 0
doc/gui/examples/controls/number-min-max.py

@@ -0,0 +1,25 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+value = 50
+
+page = """
+<|{value}|number|min=10|max=60|>
+"""
+
+Gui(page).run()
+

+ 25 - 0
doc/gui/examples/controls/number-step.py

@@ -0,0 +1,25 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+value = 50
+
+page = """
+<|{value}|number|step=2|>
+"""
+
+Gui(page).run()
+

+ 38 - 8
frontend/taipy-gui/base/src/app.ts

@@ -6,11 +6,13 @@ import { Socket, io } from "socket.io-client";
 import { DataManager, ModuleData } from "./dataManager";
 import { DataManager, ModuleData } from "./dataManager";
 import { initSocket } from "./socket";
 import { initSocket } from "./socket";
 import { TaipyWsAdapter, WsAdapter } from "./wsAdapter";
 import { TaipyWsAdapter, WsAdapter } from "./wsAdapter";
+import { WsMessageType } from "../../src/context/wsUtils";
 
 
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
 export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
 export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
 export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
 export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
+export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void;
 type Route = [string, string];
 type Route = [string, string];
 
 
 export class TaipyApp {
 export class TaipyApp {
@@ -19,6 +21,8 @@ export class TaipyApp {
     _onChange: OnChangeHandler | undefined;
     _onChange: OnChangeHandler | undefined;
     _onNotify: OnNotifyHandler | undefined;
     _onNotify: OnNotifyHandler | undefined;
     _onReload: OnReloadHandler | undefined;
     _onReload: OnReloadHandler | undefined;
+    _onWsStatusUpdate: OnWsStatusUpdate | undefined;
+    _ackList: string[];
     variableData: DataManager | undefined;
     variableData: DataManager | undefined;
     functionData: DataManager | undefined;
     functionData: DataManager | undefined;
     appId: string;
     appId: string;
@@ -33,7 +37,7 @@ export class TaipyApp {
         onInit: OnInitHandler | undefined = undefined,
         onInit: OnInitHandler | undefined = undefined,
         onChange: OnChangeHandler | undefined = undefined,
         onChange: OnChangeHandler | undefined = undefined,
         path: string | undefined = undefined,
         path: string | undefined = undefined,
-        socket: Socket | undefined = undefined,
+        socket: Socket | undefined = undefined
     ) {
     ) {
         socket = socket || io("/", { autoConnect: false });
         socket = socket || io("/", { autoConnect: false });
         this.onInit = onInit;
         this.onInit = onInit;
@@ -48,6 +52,7 @@ export class TaipyApp {
         this.path = path;
         this.path = path;
         this.socket = socket;
         this.socket = socket;
         this.wsAdapters = [new TaipyWsAdapter()];
         this.wsAdapters = [new TaipyWsAdapter()];
+        this._ackList = [];
         // Init socket io connection
         // Init socket io connection
         initSocket(socket, this);
         initSocket(socket, this);
     }
     }
@@ -91,11 +96,21 @@ export class TaipyApp {
     }
     }
     set onReload(handler: OnReloadHandler | undefined) {
     set onReload(handler: OnReloadHandler | undefined) {
         if (handler !== undefined && handler?.length !== 2) {
         if (handler !== undefined && handler?.length !== 2) {
-            throw new Error("_onReload() requires two parameters");
+            throw new Error("onReload() requires two parameters");
         }
         }
         this._onReload = handler;
         this._onReload = handler;
     }
     }
 
 
+    get onWsStatusUpdate() {
+        return this._onWsStatusUpdate;
+    }
+    set onWsStatusUpdate(handler: OnWsStatusUpdate | undefined) {
+        if (handler !== undefined && handler?.length !== 2) {
+            throw new Error("onWsStatusUpdate() requires two parameters");
+        }
+        this._onWsStatusUpdate = handler;
+    }
+
     // Utility methods
     // Utility methods
     init() {
     init() {
         this.clientId = "";
         this.clientId = "";
@@ -103,15 +118,26 @@ export class TaipyApp {
         this.appId = "";
         this.appId = "";
         this.routes = undefined;
         this.routes = undefined;
         const id = getLocalStorageValue(TAIPY_CLIENT_ID, "");
         const id = getLocalStorageValue(TAIPY_CLIENT_ID, "");
-        sendWsMessage(this.socket, "ID", TAIPY_CLIENT_ID, id, id, undefined, false);
-        sendWsMessage(this.socket, "AID", "connect", "", id, undefined, false);
-        sendWsMessage(this.socket, "GR", "", "", id, undefined, false);
+        this.sendWsMessage("ID", TAIPY_CLIENT_ID, id);
+        this.sendWsMessage("AID", "connect", "");
+        this.sendWsMessage("GR", "", "");
         if (id !== "") {
         if (id !== "") {
             this.clientId = id;
             this.clientId = id;
             this.updateContext(this.path);
             this.updateContext(this.path);
         }
         }
     }
     }
 
 
+    sendWsMessage(type: WsMessageType, id: string, payload: unknown, context: string | undefined = undefined) {
+        if (context === undefined) {
+            context = this.context;
+        }
+        const ackId = sendWsMessage(this.socket, type, id, payload, this.clientId, context);
+        if (ackId) {
+            this._ackList.push(ackId);
+            this.onWsStatusUpdate && this.onWsStatusUpdate(this, this._ackList);
+        }
+    }
+
     // Public methods
     // Public methods
     registerWsAdapter(wsAdapter: WsAdapter) {
     registerWsAdapter(wsAdapter: WsAdapter) {
         this.wsAdapters.unshift(wsAdapter);
         this.wsAdapters.unshift(wsAdapter);
@@ -153,7 +179,7 @@ export class TaipyApp {
     // This update will only send the request to Taipy Gui backend
     // This update will only send the request to Taipy Gui backend
     // the actual update will be handled when the backend responds
     // the actual update will be handled when the backend responds
     update(encodedName: string, value: unknown) {
     update(encodedName: string, value: unknown) {
-        sendWsMessage(this.socket, "U", encodedName, { value: value }, this.clientId, this.context);
+        this.sendWsMessage("U", encodedName, { value: value });
     }
     }
 
 
     getContext() {
     getContext() {
@@ -164,12 +190,12 @@ export class TaipyApp {
         if (!path || path === "") {
         if (!path || path === "") {
             path = window.location.pathname.slice(1);
             path = window.location.pathname.slice(1);
         }
         }
-        sendWsMessage(this.socket, "GMC", "get_module_context", { path: path || "/" }, this.clientId);
+        this.sendWsMessage("GMC", "get_module_context", { path: path || "/" });
     }
     }
 
 
     trigger(actionName: string, triggerId: string, payload: Record<string, unknown> = {}) {
     trigger(actionName: string, triggerId: string, payload: Record<string, unknown> = {}) {
         payload["action"] = actionName;
         payload["action"] = actionName;
-        sendWsMessage(this.socket, "A", triggerId, payload, this.clientId, this.context);
+        this.sendWsMessage("A", triggerId, payload);
     }
     }
 
 
     upload(encodedName: string, files: FileList, progressCallback: (val: number) => void) {
     upload(encodedName: string, files: FileList, progressCallback: (val: number) => void) {
@@ -179,6 +205,10 @@ export class TaipyApp {
     getPageMetadata() {
     getPageMetadata() {
         return this.metadata;
         return this.metadata;
     }
     }
+
+    getWsStatus() {
+        return this._ackList;
+    }
 }
 }
 
 
 export const createApp = (onInit?: OnInitHandler, onChange?: OnChangeHandler, path?: string, socket?: Socket) => {
 export const createApp = (onInit?: OnInitHandler, onChange?: OnChangeHandler, path?: string, socket?: Socket) => {

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

@@ -1,9 +1,7 @@
 import { WsAdapter } from "./wsAdapter";
 import { WsAdapter } from "./wsAdapter";
-import { sendWsMessage } from "../../src/context/wsUtils";
 // import { TaipyApp } from "./app";
 // import { TaipyApp } from "./app";
 
 
 export {
 export {
     WsAdapter,
     WsAdapter,
-    sendWsMessage,
     // TaipyApp,
     // TaipyApp,
 };
 };

+ 28 - 30
frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts

@@ -20,10 +20,29 @@ declare class DataManager {
     getAllData(): Record<string, unknown>;
     getAllData(): Record<string, unknown>;
     update(encodedName: string, value: unknown): void;
     update(encodedName: string, value: unknown): void;
 }
 }
+export type WsMessageType =
+    | "A"
+    | "U"
+    | "DU"
+    | "MU"
+    | "RU"
+    | "AL"
+    | "BL"
+    | "NA"
+    | "ID"
+    | "MS"
+    | "DF"
+    | "PR"
+    | "ACK"
+    | "GMC"
+    | "GDT"
+    | "AID"
+    | "GR";
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
 export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
 export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
 export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
 export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
+export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void;
 export type Route = [string, string];
 export type Route = [string, string];
 export declare class TaipyApp {
 export declare class TaipyApp {
     socket: Socket;
     socket: Socket;
@@ -31,11 +50,14 @@ export declare class TaipyApp {
     _onChange: OnChangeHandler | undefined;
     _onChange: OnChangeHandler | undefined;
     _onNotify: OnNotifyHandler | undefined;
     _onNotify: OnNotifyHandler | undefined;
     _onReload: OnReloadHandler | undefined;
     _onReload: OnReloadHandler | undefined;
+    _onWsStatusUpdate: OnWsStatusUpdate | undefined;
+    _ackList: string[];
     variableData: DataManager | undefined;
     variableData: DataManager | undefined;
     functionData: DataManager | undefined;
     functionData: DataManager | undefined;
     appId: string;
     appId: string;
     clientId: string;
     clientId: string;
     context: string;
     context: string;
+    metadata: Record<string, unknown>;
     path: string | undefined;
     path: string | undefined;
     routes: Route[] | undefined;
     routes: Route[] | undefined;
     wsAdapters: WsAdapter[];
     wsAdapters: WsAdapter[];
@@ -53,7 +75,10 @@ export declare class TaipyApp {
     set onNotify(handler: OnNotifyHandler | undefined);
     set onNotify(handler: OnNotifyHandler | undefined);
     get onReload(): OnReloadHandler | undefined;
     get onReload(): OnReloadHandler | undefined;
     set onReload(handler: OnReloadHandler | undefined);
     set onReload(handler: OnReloadHandler | undefined);
+    get onWsStatusUpdate(): OnWsStatusUpdate | undefined;
+    set onWsStatusUpdate(handler: OnWsStatusUpdate | undefined);
     init(): void;
     init(): void;
+    sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void;
     registerWsAdapter(wsAdapter: WsAdapter): void;
     registerWsAdapter(wsAdapter: WsAdapter): void;
     getEncodedName(varName: string, module: string): string | undefined;
     getEncodedName(varName: string, module: string): string | undefined;
     getName(encodedName: string): [string, string] | undefined;
     getName(encodedName: string): [string, string] | undefined;
@@ -68,28 +93,11 @@ export declare class TaipyApp {
     updateContext(path?: string | undefined): void;
     updateContext(path?: string | undefined): void;
     trigger(actionName: string, triggerId: string, payload?: Record<string, unknown>): void;
     trigger(actionName: string, triggerId: string, payload?: Record<string, unknown>): void;
     upload(encodedName: string, files: FileList, progressCallback: (val: number) => void): Promise<string>;
     upload(encodedName: string, files: FileList, progressCallback: (val: number) => void): Promise<string>;
-    getPageMetadata(): any;
+    getPageMetadata(): Record<string, unknown>;
+    getWsStatus(): string[];
 }
 }
-export type WsMessageType =
-    | "A"
-    | "U"
-    | "DU"
-    | "MU"
-    | "RU"
-    | "AL"
-    | "BL"
-    | "NA"
-    | "ID"
-    | "MS"
-    | "DF"
-    | "PR"
-    | "ACK"
-    | "GMC"
-    | "GDT"
-    | "AID"
-    | "GR";
 export interface WsMessage {
 export interface WsMessage {
-    type: WsMessageType | str;
+    type: WsMessageType | string;
     name: string;
     name: string;
     payload: Record<string, unknown> | unknown;
     payload: Record<string, unknown> | unknown;
     propagate: boolean;
     propagate: boolean;
@@ -97,16 +105,6 @@ export interface WsMessage {
     module_context: string;
     module_context: string;
     ack_id?: string;
     ack_id?: string;
 }
 }
-export declare const sendWsMessage: (
-    socket: Socket | undefined,
-    type: WsMessageType | str,
-    name: string,
-    payload: Record<string, unknown> | unknown,
-    id: string,
-    moduleContext?: string,
-    propagate?: boolean,
-    serverAck?: (val: unknown) => void
-) => string;
 export declare abstract class WsAdapter {
 export declare abstract class WsAdapter {
     abstract supportedMessageTypes: string[];
     abstract supportedMessageTypes: string[];
     abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;
     abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;

+ 2 - 2
frontend/taipy-gui/base/src/socket.ts

@@ -1,5 +1,5 @@
 import { Socket } from "socket.io-client";
 import { Socket } from "socket.io-client";
-import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
+import { WsMessage } from "../../src/context/wsUtils";
 import { TaipyApp } from "./app";
 import { TaipyApp } from "./app";
 
 
 export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
 export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
@@ -11,7 +11,7 @@ export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
     // Send a request to get App ID to verify that the app has not been reloaded
     // Send a request to get App ID to verify that the app has not been reloaded
     socket.io.on("reconnect", () => {
     socket.io.on("reconnect", () => {
         console.log("WebSocket reconnected");
         console.log("WebSocket reconnected");
-        sendWsMessage(socket, "AID", "reconnect", taipyApp.appId, taipyApp.clientId, taipyApp.context);
+        taipyApp.sendWsMessage("AID", "reconnect", taipyApp.appId);
     });
     });
     // try to reconnect on connect_error
     // try to reconnect on connect_error
     socket.on("connect_error", (err) => {
     socket.on("connect_error", (err) => {

+ 7 - 3
frontend/taipy-gui/base/src/wsAdapter.ts

@@ -1,7 +1,7 @@
 import merge from "lodash/merge";
 import merge from "lodash/merge";
 import { TaipyApp } from "./app";
 import { TaipyApp } from "./app";
 import { IdMessage, storeClientId } from "../../src/context/utils";
 import { IdMessage, storeClientId } from "../../src/context/utils";
-import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
+import { WsMessage } from "../../src/context/wsUtils";
 import { DataManager, ModuleData } from "./dataManager";
 import { DataManager, ModuleData } from "./dataManager";
 
 
 export abstract class WsAdapter {
 export abstract class WsAdapter {
@@ -25,7 +25,7 @@ export class TaipyWsAdapter extends WsAdapter {
     initWsMessageTypes: string[];
     initWsMessageTypes: string[];
     constructor() {
     constructor() {
         super();
         super();
-        this.supportedMessageTypes = ["MU", "ID", "GMC", "GDT", "AID", "GR", "AL"];
+        this.supportedMessageTypes = ["MU", "ID", "GMC", "GDT", "AID", "GR", "AL", "ACK"];
         this.initWsMessageTypes = ["ID", "AID", "GMC"];
         this.initWsMessageTypes = ["ID", "AID", "GMC"];
     }
     }
     handleWsMessage(message: WsMessage, taipyApp: TaipyApp): boolean {
     handleWsMessage(message: WsMessage, taipyApp: TaipyApp): boolean {
@@ -81,6 +81,10 @@ export class TaipyWsAdapter extends WsAdapter {
             } else if (message.type === "AL" && taipyApp.onNotify) {
             } else if (message.type === "AL" && taipyApp.onNotify) {
                 const payload = message as AlertMessage;
                 const payload = message as AlertMessage;
                 taipyApp.onNotify(taipyApp, payload.atype, payload.message);
                 taipyApp.onNotify(taipyApp, payload.atype, payload.message);
+            } else if (message.type === "ACK") {
+                const {id} = message as unknown as Record<string, string>;
+                taipyApp._ackList = taipyApp._ackList.filter((v) => v !== id);
+                taipyApp.onWsStatusUpdate && taipyApp.onWsStatusUpdate(taipyApp, taipyApp._ackList);
             }
             }
             this.postWsMessageProcessing(message, taipyApp);
             this.postWsMessageProcessing(message, taipyApp);
             return true;
             return true;
@@ -96,7 +100,7 @@ export class TaipyWsAdapter extends WsAdapter {
             taipyApp.context !== "" &&
             taipyApp.context !== "" &&
             taipyApp.routes !== undefined
             taipyApp.routes !== undefined
         ) {
         ) {
-            sendWsMessage(taipyApp.socket, "GDT", "get_data_tree", {}, taipyApp.clientId, taipyApp.context);
+            taipyApp.sendWsMessage("GDT", "get_data_tree", {});
         }
         }
     }
     }
 }
 }

Разница между файлами не показана из-за своего большого размера
+ 254 - 233
frontend/taipy-gui/package-lock.json


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

@@ -29,7 +29,7 @@
     "react-window-infinite-loader": "^1.0.7",
     "react-window-infinite-loader": "^1.0.7",
     "socket.io-client": "^4.3.2",
     "socket.io-client": "^4.3.2",
     "sprintf-js": "^1.1.2",
     "sprintf-js": "^1.1.2",
-    "uuid": "^9.0.0"
+    "uuid": "^10.0.0"
   },
   },
   "overrides": {
   "overrides": {
     "react": "$react",
     "react": "$react",
@@ -75,7 +75,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@testing-library/jest-dom": "^6.1.3",
     "@testing-library/jest-dom": "^6.1.3",
-    "@testing-library/react": "^15.0.7",
+    "@testing-library/react": "^16.0.0",
     "@testing-library/user-event": "^14.2.1",
     "@testing-library/user-event": "^14.2.1",
     "@types/css-mediaquery": "^0.1.1",
     "@types/css-mediaquery": "^0.1.1",
     "@types/jest": "^29.0.1",
     "@types/jest": "^29.0.1",
@@ -88,7 +88,7 @@
     "@types/react-window": "^1.8.5",
     "@types/react-window": "^1.8.5",
     "@types/react-window-infinite-loader": "^1.0.5",
     "@types/react-window-infinite-loader": "^1.0.5",
     "@types/sprintf-js": "^1.1.2",
     "@types/sprintf-js": "^1.1.2",
-    "@types/uuid": "^9.0.0",
+    "@types/uuid": "^10.0.0",
     "@typescript-eslint/eslint-plugin": "^7.0.1",
     "@typescript-eslint/eslint-plugin": "^7.0.1",
     "@typescript-eslint/parser": "^7.0.1",
     "@typescript-eslint/parser": "^7.0.1",
     "add-asset-html-webpack-plugin": "^6.0.0",
     "add-asset-html-webpack-plugin": "^6.0.0",
@@ -102,7 +102,7 @@
     "eslint": "^8.57.0",
     "eslint": "^8.57.0",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "eslint-plugin-tsdoc": "^0.2.16",
+    "eslint-plugin-tsdoc": "^0.3.0",
     "eslint-webpack-plugin": "^4.0.0",
     "eslint-webpack-plugin": "^4.0.0",
     "generate-json-webpack-plugin": "^2.0.0",
     "generate-json-webpack-plugin": "^2.0.0",
     "html-webpack-plugin": "^5.5.0",
     "html-webpack-plugin": "^5.5.0",
@@ -115,9 +115,9 @@
     "mock-xmlhttprequest": "^8.2.0",
     "mock-xmlhttprequest": "^8.2.0",
     "ts-jest": "^29.0.0",
     "ts-jest": "^29.0.0",
     "ts-loader": "^9.2.6",
     "ts-loader": "^9.2.6",
-    "typedoc": "^0.25.1",
-    "typedoc-plugin-markdown": "^3.13.4",
-    "typescript": "^5.2.2",
+    "typedoc": "^0.26.3",
+    "typedoc-plugin-markdown": "^4.1.1",
+    "typescript": "^5.5.3",
     "webpack": "^5.61.0",
     "webpack": "^5.61.0",
     "webpack-cli": "^5.0.0"
     "webpack-cli": "^5.0.0"
   }
   }

+ 1 - 27
frontend/taipy-gui/src/components/Taipy/DateRange.tsx

@@ -21,7 +21,7 @@ import { isValid } from "date-fns";
 import { ErrorBoundary } from "react-error-boundary";
 import { ErrorBoundary } from "react-error-boundary";
 
 
 import { createSendUpdateAction } from "../../context/taipyReducers";
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps } from "./utils";
+import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps } from "./utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import Field from "./Field";
 import Field from "./Field";
@@ -60,32 +60,6 @@ const getRangeDateTime = (
     return [null, null];
     return [null, null];
 };
 };
 
 
-interface DateProps {
-    maxDate?: unknown;
-    maxDateTime?: unknown;
-    maxTime?: unknown;
-    minDate?: unknown;
-    minDateTime?: unknown;
-    minTime?: unknown;
-}
-
-const getProps = (p: DateProps, start: boolean, val: Date | null, withTime: boolean): DateProps => {
-    if (!val) {
-        return {};
-    }
-    const propName: keyof DateProps = withTime
-        ? start
-            ? "minDateTime"
-            : "maxDateTime"
-        : start
-        ? "minDate"
-        : "maxDate";
-    if (p[propName] == val) {
-        return p;
-    }
-    return { ...p, [propName]: val };
-};
-
 const DateRange = (props: DateRangeProps) => {
 const DateRange = (props: DateRangeProps) => {
     const { updateVarName, withTime = false, id, propagate = true } = props;
     const { updateVarName, withTime = false, id, propagate = true } = props;
     const dispatch = useDispatch();
     const dispatch = useDispatch();

+ 33 - 9
frontend/taipy-gui/src/components/Taipy/DateSelector.tsx

@@ -14,14 +14,14 @@
 import React, { useState, useEffect, useCallback } from "react";
 import React, { useState, useEffect, useCallback } from "react";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
-import { DatePicker } from "@mui/x-date-pickers/DatePicker";
+import { DatePicker, DatePickerProps } from "@mui/x-date-pickers/DatePicker";
 import { BaseDateTimePickerSlotProps } from "@mui/x-date-pickers/DateTimePicker/shared";
 import { BaseDateTimePickerSlotProps } from "@mui/x-date-pickers/DateTimePicker/shared";
-import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
+import { DateTimePicker, DateTimePickerProps } from "@mui/x-date-pickers/DateTimePicker";
 import { isValid } from "date-fns";
 import { isValid } from "date-fns";
 import { ErrorBoundary } from "react-error-boundary";
 import { ErrorBoundary } from "react-error-boundary";
 
 
 import { createSendUpdateAction } from "../../context/taipyReducers";
 import { createSendUpdateAction } from "../../context/taipyReducers";
-import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps } from "./utils";
+import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps, DateProps, getProps } from "./utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
 import Field from "./Field";
 import Field from "./Field";
@@ -31,6 +31,10 @@ interface DateSelectorProps extends TaipyActiveProps, TaipyChangeProps {
     withTime?: boolean;
     withTime?: boolean;
     format?: string;
     format?: string;
     date: string;
     date: string;
+    min?: string;
+    defaultMin?: string;
+    max?: string;
+    defaultMax?: string;
     defaultDate?: string;
     defaultDate?: string;
     defaultEditable?: boolean;
     defaultEditable?: boolean;
     editable?: boolean;
     editable?: boolean;
@@ -46,12 +50,16 @@ const DateSelector = (props: DateSelectorProps) => {
     const formatConfig = useFormatConfig();
     const formatConfig = useFormatConfig();
     const tz = formatConfig.timeZone;
     const tz = formatConfig.timeZone;
     const [value, setValue] = useState(() => getDateTime(props.defaultDate, tz, withTime));
     const [value, setValue] = useState(() => getDateTime(props.defaultDate, tz, withTime));
+    const [startProps, setStartProps] = useState<DateProps>({});
+    const [endProps, setEndProps] = useState<DateProps>({});
     const module = useModule();
     const module = useModule();
 
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const editable = useDynamicProperty(props.editable, props.defaultEditable, true);
     const editable = useDynamicProperty(props.editable, props.defaultEditable, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
+    const min = useDynamicProperty(props.min, props.defaultMin, undefined);
+    const max = useDynamicProperty(props.max, props.defaultMax, undefined);
 
 
     const handleChange = useCallback(
     const handleChange = useCallback(
         (v: Date | null) => {
         (v: Date | null) => {
@@ -64,20 +72,32 @@ const DateSelector = (props: DateSelectorProps) => {
                         dateToString(newDate, withTime),
                         dateToString(newDate, withTime),
                         module,
                         module,
                         props.onChange,
                         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
     // Run every time props.value get updated
     useEffect(() => {
     useEffect(() => {
-        if (props.date !== undefined) {
-            setValue(getDateTime(props.date, tz, withTime));
+        try {
+            if (props.date !== undefined) {
+                setValue(getDateTime(props.date, tz, withTime));
+            }
+
+            if (min !== undefined) {
+                setStartProps((p) => getProps(p, true, getDateTime(min, tz, withTime), withTime));
+            }
+
+            if (max !== undefined) {
+                setEndProps((p) => getProps(p, false, getDateTime(max, tz, withTime), withTime));
+            }
+        } catch (error) {
+            console.error(error);
         }
         }
-    }, [props.date, tz, withTime]);
+    }, [props.date, tz, withTime, max, min]);
 
 
     return (
     return (
         <ErrorBoundary FallbackComponent={ErrorFallback}>
         <ErrorBoundary FallbackComponent={ErrorFallback}>
@@ -86,6 +106,8 @@ const DateSelector = (props: DateSelectorProps) => {
                     {editable ? (
                     {editable ? (
                         withTime ? (
                         withTime ? (
                             <DateTimePicker
                             <DateTimePicker
+                                {...(startProps as DateTimePickerProps<Date>)}
+                                {...(endProps as DateTimePickerProps<Date>)}
                                 value={value}
                                 value={value}
                                 onChange={handleChange}
                                 onChange={handleChange}
                                 className={getSuffixedClassNames(className, "-picker")}
                                 className={getSuffixedClassNames(className, "-picker")}
@@ -96,6 +118,8 @@ const DateSelector = (props: DateSelectorProps) => {
                             />
                             />
                         ) : (
                         ) : (
                             <DatePicker
                             <DatePicker
+                                {...(startProps as DatePickerProps<Date>)}
+                                {...(endProps as DatePickerProps<Date>)}
                                 value={value}
                                 value={value}
                                 onChange={handleChange}
                                 onChange={handleChange}
                                 className={getSuffixedClassNames(className, "-picker")}
                                 className={getSuffixedClassNames(className, "-picker")}

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

@@ -28,14 +28,14 @@ describe("Input Component", () => {
     });
     });
     it("displays the right info for string", async () => {
     it("displays the right info for string", async () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
-            <Input value="toto" type="text" defaultValue="titi" className="taipy-input" />
+            <Input value="toto" type="text" defaultValue="titi" className="taipy-input" />,
         );
         );
         const elt = getByDisplayValue("toto");
         const elt = getByDisplayValue("toto");
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-input");
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-input");
     });
     });
     it("displays the default value", async () => {
     it("displays the default value", async () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
-            <Input defaultValue="titi" value={undefined as unknown as string} type="text" />
+            <Input defaultValue="titi" value={undefined as unknown as string} type="text" />,
         );
         );
         getByDisplayValue("titi");
         getByDisplayValue("titi");
     });
     });
@@ -60,7 +60,7 @@ describe("Input Component", () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Input value="Val" type="text" updateVarName="varname" />
                 <Input value="Val" type="text" updateVarName="varname" />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         );
         const elt = getByDisplayValue("Val");
         const elt = getByDisplayValue("Val");
         await userEvent.clear(elt);
         await userEvent.clear(elt);
@@ -78,7 +78,7 @@ describe("Input Component", () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
                 <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         );
         const elt = getByDisplayValue("Val");
         const elt = getByDisplayValue("Val");
         await userEvent.click(elt);
         await userEvent.click(elt);
@@ -96,7 +96,7 @@ describe("Input Component", () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Input value="Val" type="text" updateVarName="varname" changeDelay={-1} />
                 <Input value="Val" type="text" updateVarName="varname" changeDelay={-1} />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         );
         const elt = getByDisplayValue("Val");
         const elt = getByDisplayValue("Val");
         await userEvent.click(elt);
         await userEvent.click(elt);
@@ -115,7 +115,7 @@ describe("Input Component", () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
                 <Input value="Val" type="text" updateVarName="varname" onAction="on_action" />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         );
         const elt = getByDisplayValue("Val");
         const elt = getByDisplayValue("Val");
         await userEvent.click(elt);
         await userEvent.click(elt);
@@ -138,14 +138,14 @@ describe("Number Component", () => {
     });
     });
     it("displays the right info for string", async () => {
     it("displays the right info for string", async () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
-            <Input value="12" type="number" defaultValue="1" className="taipy-number" />
+            <Input value="12" type="number" defaultValue="1" className="taipy-number" />,
         );
         );
         const elt = getByDisplayValue(12);
         const elt = getByDisplayValue(12);
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-number");
         expect(elt.parentElement?.parentElement).toHaveClass("taipy-number");
     });
     });
     it("displays the default value", async () => {
     it("displays the default value", async () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
-            <Input defaultValue="1" value={undefined as unknown as string} type="number" />
+            <Input defaultValue="1" value={undefined as unknown as string} type="number" />,
         );
         );
         getByDisplayValue("1");
         getByDisplayValue("1");
     });
     });
@@ -170,7 +170,7 @@ describe("Number Component", () => {
         const { getByDisplayValue } = render(
         const { getByDisplayValue } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
             <TaipyContext.Provider value={{ state, dispatch }}>
                 <Input value={"33"} type="number" updateVarName="varname" />
                 <Input value={"33"} type="number" updateVarName="varname" />
-            </TaipyContext.Provider>
+            </TaipyContext.Provider>,
         );
         );
         const elt = getByDisplayValue("33");
         const elt = getByDisplayValue("33");
         await userEvent.clear(elt);
         await userEvent.clear(elt);
@@ -184,8 +184,8 @@ describe("Number Component", () => {
         });
         });
     });
     });
     xit("shows 0", async () => {
     xit("shows 0", async () => {
-    //not working cf. https://github.com/testing-library/user-event/issues/1066
-    const { getByDisplayValue, rerender } = render(<Input value={"0"} type="number" />);
+        //not working cf. https://github.com/testing-library/user-event/issues/1066
+        const { getByDisplayValue, rerender } = render(<Input value={"0"} type="number" />);
         const elt = getByDisplayValue("0") as HTMLInputElement;
         const elt = getByDisplayValue("0") as HTMLInputElement;
         expect(elt).toBeInTheDocument();
         expect(elt).toBeInTheDocument();
         await userEvent.type(elt, "{ArrowUp}");
         await userEvent.type(elt, "{ArrowUp}");
@@ -193,4 +193,67 @@ describe("Number Component", () => {
         await userEvent.type(elt, "{ArrowDown}");
         await userEvent.type(elt, "{ArrowDown}");
         expect(elt.value).toBe("0");
         expect(elt.value).toBe("0");
     });
     });
+    it("Validates increment by step value on up click", async () => {
+        const { getByDisplayValue, getByTestId } = render(<Input value={"0"} type="number" step={2} />);
+        const upSpinner = getByTestId("stepper-up-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await userEvent.click(upSpinner);
+        expect(elt.value).toBe("2");
+    });
+    it("Validates decrement by step value on down click", async () => {
+        const { getByDisplayValue, getByTestId } = render(<Input value={"0"} type="number" step={2} />);
+        const downSpinner = getByTestId("stepper-down-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await userEvent.click(downSpinner);
+        expect(elt.value).toBe("-2");
+    });
+    it("Validates increment when holding shift key and clicking up", async () => {
+        const user = userEvent.setup();
+        const { getByDisplayValue, getByTestId } = render(<Input value={"0"} type="number" step={2} />);
+        const upSpinner = getByTestId("stepper-up-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.keyboard("[ShiftLeft>]");
+        await user.click(upSpinner);
+        expect(elt.value).toBe("20");
+    });
+    it("Validates decrement when holding shift key and clicking down", async () => {
+        const user = userEvent.setup();
+        const { getByDisplayValue, getByTestId } = render(<Input value={"0"} type="number" step={2} />);
+        const downSpinner = getByTestId("stepper-down-spinner");
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.keyboard("[ShiftLeft>]");
+        await user.click(downSpinner);
+        expect(elt.value).toBe("-20");
+    });
+    it("Validate increment when holding shift key and arrow up", async () => {
+        const user = userEvent.setup();
+        const { getByDisplayValue } = render(<Input value={"0"} type="number" step={2} />);
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.click(elt);
+        await user.keyboard("[ShiftLeft>]");
+        await user.keyboard("[ArrowUp]");
+        expect(elt.value).toBe("20");
+    });
+    it("Validate value when reaching max value", async () => {
+        const user = userEvent.setup();
+        const { getByDisplayValue } = render(<Input value={"0"} type="number" step={2} max={20} />);
+        const elt = getByDisplayValue("0") as HTMLInputElement;
+        await user.click(elt);
+        await user.keyboard("[ShiftLeft>]");
+        // Press the arrow up twice to validate that the value will not exceed the maximum value when reached
+        await user.keyboard("[ArrowUp]");
+        await user.keyboard("[ArrowUp]");
+        expect(elt.value).toBe("20");
+    });
+    it("Validate value when reaching min value", async () => {
+        const user = userEvent.setup();
+        const { getByDisplayValue } = render(<Input value={"20"} type="number" step={2} min={0} />);
+        const elt = getByDisplayValue("20") as HTMLInputElement;
+        await user.click(elt);
+        await user.keyboard("[ShiftLeft>]");
+        // Press the arrow down twice to validate that the value will not exceed the minimum value when reached
+        await user.keyboard("[ArrowDown]");
+        await user.keyboard("[ArrowDown]");
+        expect(elt.value).toBe("0");
+    });
 });
 });

+ 186 - 5
frontend/taipy-gui/src/components/Taipy/Input.tsx

@@ -11,9 +11,15 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useState, useEffect, useCallback, useRef, KeyboardEvent } from "react";
+import React, { useState, useEffect, useCallback, useRef, KeyboardEvent, useMemo, CSSProperties } from "react";
+import IconButton from "@mui/material/IconButton";
+import InputAdornment from "@mui/material/InputAdornment";
 import TextField from "@mui/material/TextField";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
+import Visibility from "@mui/icons-material/Visibility";
+import VisibilityOff from "@mui/icons-material/VisibilityOff";
+import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
 
 
 import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers";
 import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers";
 import { TaipyInputProps } from "./utils";
 import { TaipyInputProps } from "./utils";
@@ -33,6 +39,20 @@ const getActionKeys = (keys?: string): string[] => {
     return ak.length > 0 ? ak : [AUTHORIZED_KEYS[0]];
     return ak.length > 0 ? ak : [AUTHORIZED_KEYS[0]];
 };
 };
 
 
+const numberSx = {
+    "& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button": {
+        display: "none",
+    },
+    "& input[type=number]": {
+        MozAppearance: "textfield",
+    },
+};
+const verticalDivStyle: CSSProperties = {
+    display: "flex",
+    flexDirection: "column",
+    gap: 0,
+};
+
 const Input = (props: TaipyInputProps) => {
 const Input = (props: TaipyInputProps) => {
     const {
     const {
         type,
         type,
@@ -45,6 +65,7 @@ const Input = (props: TaipyInputProps) => {
         multiline = false,
         multiline = false,
         linesShown = 5,
         linesShown = 5,
     } = props;
     } = props;
+
     const [value, setValue] = useState(defaultValue);
     const [value, setValue] = useState(defaultValue);
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const delayCall = useRef(-1);
     const delayCall = useRef(-1);
@@ -55,6 +76,10 @@ const Input = (props: TaipyInputProps) => {
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
+    const step = useDynamicProperty(props.step, props.defaultStep, 1);
+    const stepMultiplier = useDynamicProperty(props.stepMultiplier, props.defaultStepMultiplier, 10);
+    const min = useDynamicProperty(props.min, props.defaultMin, undefined);
+    const max = useDynamicProperty(props.max, props.defaultMax, undefined);
 
 
     const handleInput = useCallback(
     const handleInput = useCallback(
         (e: React.ChangeEvent<HTMLInputElement>) => {
         (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -79,7 +104,27 @@ const Input = (props: TaipyInputProps) => {
 
 
     const handleAction = useCallback(
     const handleAction = useCallback(
         (evt: KeyboardEvent<HTMLDivElement>) => {
         (evt: KeyboardEvent<HTMLDivElement>) => {
-            if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
+            if (evt.shiftKey && type === "number") {
+                if (evt.key === "ArrowUp") {
+                    let val =
+                        Number(evt.currentTarget.querySelector("input")?.value || 0) +
+                        (step || 1) * (stepMultiplier || 10);
+                    if (max !== undefined && val > max) {
+                        val = max;
+                    }
+                    setValue(val.toString());
+                    evt.preventDefault();
+                } else if (evt.key === "ArrowDown") {
+                    let val =
+                        Number(evt.currentTarget.querySelector("input")?.value || 0) -
+                        (step || 1) * (stepMultiplier || 10);
+                    if (min !== undefined && val < min) {
+                        val = min;
+                    }
+                    setValue(val.toString());
+                    evt.preventDefault();
+                }
+            } else if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
                 const val = evt.currentTarget.querySelector("input")?.value;
                 const val = evt.currentTarget.querySelector("input")?.value;
                 if (changeDelay > 0 && delayCall.current > 0) {
                 if (changeDelay > 0 && delayCall.current > 0) {
                     clearTimeout(delayCall.current);
                     clearTimeout(delayCall.current);
@@ -92,7 +137,141 @@ const Input = (props: TaipyInputProps) => {
                 evt.preventDefault();
                 evt.preventDefault();
             }
             }
         },
         },
-        [actionKeys, updateVarName, onAction, id, dispatch, onChange, changeDelay, propagate, module]
+        [
+            type,
+            actionKeys,
+            step,
+            stepMultiplier,
+            max,
+            min,
+            changeDelay,
+            onAction,
+            dispatch,
+            id,
+            module,
+            updateVarName,
+            onChange,
+            propagate,
+        ]
+    );
+
+    const roundBasedOnStep = useMemo(() => {
+        const stepString = (step || 1).toString();
+        const decimalPlaces = stepString.includes(".") ? stepString.split(".")[1].length : 0;
+        const multiplier = Math.pow(10, decimalPlaces);
+        return (value: number) => Math.round(value * multiplier) / multiplier;
+    }, [step]);
+
+    const calculateNewValue = useMemo(() => {
+        return (prevValue: string, step: number, stepMultiplier: number, shiftKey: boolean, increment: boolean) => {
+            const multiplier = shiftKey ? stepMultiplier : 1;
+            const change = step * multiplier * (increment ? 1 : -1);
+            return roundBasedOnStep(Number(prevValue) + change).toString();
+        };
+    }, [roundBasedOnStep]);
+
+    const handleStepperMouseDown = useCallback(
+        (event: React.MouseEvent<HTMLButtonElement>, increment: boolean) => {
+            setValue((prevValue) => {
+                const newValue = calculateNewValue(
+                    prevValue,
+                    step || 1,
+                    stepMultiplier || 10,
+                    event.shiftKey,
+                    increment
+                );
+                if (min !== undefined && Number(newValue) < min) {
+                    return min.toString();
+                }
+                if (max !== undefined && Number(newValue) > max) {
+                    return max.toString();
+                }
+                return newValue;
+            });
+        },
+        [min, max, step, stepMultiplier, calculateNewValue]
+    );
+
+    const handleUpStepperMouseDown = useCallback(
+        (event: React.MouseEvent<HTMLButtonElement>) => {
+            handleStepperMouseDown(event, true);
+        },
+        [handleStepperMouseDown]
+    );
+
+    const handleDownStepperMouseDown = useCallback(
+        (event: React.MouseEvent<HTMLButtonElement>) => {
+            handleStepperMouseDown(event, false);
+        },
+        [handleStepperMouseDown]
+    );
+
+    // password
+    const [showPassword, setShowPassword] = useState(false);
+    const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []);
+    const handleMouseDownPassword = useCallback(
+        (event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault(),
+        []
+    );
+    const muiInputProps = useMemo(
+        () =>
+            type == "password"
+                ? {
+                      endAdornment: (
+                          <InputAdornment position="end">
+                              <IconButton
+                                  aria-label="toggle password visibility"
+                                  onClick={handleClickShowPassword}
+                                  onMouseDown={handleMouseDownPassword}
+                                  edge="end"
+                              >
+                                  {showPassword ? <VisibilityOff /> : <Visibility />}
+                              </IconButton>
+                          </InputAdornment>
+                      ),
+                  }
+                : type == "number"
+                ? {
+                      endAdornment: (
+                          <div style={verticalDivStyle}>
+                              <IconButton
+                                  data-testid="stepper-up-spinner"
+                                  size="small"
+                                  onMouseDown={handleUpStepperMouseDown}
+                              >
+                                  <ArrowDropUpIcon fontSize="inherit" />
+                              </IconButton>
+                              <IconButton
+                                  data-testid="stepper-down-spinner"
+                                  size="small"
+                                  onMouseDown={handleDownStepperMouseDown}
+                              >
+                                  <ArrowDropDownIcon fontSize="inherit" />
+                              </IconButton>
+                          </div>
+                      ),
+                  }
+                : undefined,
+        [
+            type,
+            showPassword,
+            handleClickShowPassword,
+            handleMouseDownPassword,
+            handleUpStepperMouseDown,
+            handleDownStepperMouseDown,
+        ]
+    );
+
+    const inputProps = useMemo(
+        () =>
+            type == "number"
+                ? {
+                      step: step ? step : 1,
+                      min: min,
+                      max: max,
+                  }
+                : undefined,
+        [type, step, min, max]
     );
     );
 
 
     useEffect(() => {
     useEffect(() => {
@@ -104,12 +283,15 @@ const Input = (props: TaipyInputProps) => {
     return (
     return (
         <Tooltip title={hover || ""}>
         <Tooltip title={hover || ""}>
             <TextField
             <TextField
+                sx={numberSx}
                 margin="dense"
                 margin="dense"
                 hiddenLabel
                 hiddenLabel
                 value={value ?? ""}
                 value={value ?? ""}
                 className={className}
                 className={className}
-                type={type}
+                type={showPassword && type == "password" ? "text" : type}
                 id={id}
                 id={id}
+                inputProps={inputProps}
+                InputProps={muiInputProps}
                 label={props.label}
                 label={props.label}
                 onChange={handleInput}
                 onChange={handleInput}
                 disabled={!active}
                 disabled={!active}
@@ -120,5 +302,4 @@ const Input = (props: TaipyInputProps) => {
         </Tooltip>
         </Tooltip>
     );
     );
 };
 };
-
 export default Input;
 export default Input;

+ 43 - 34
frontend/taipy-gui/src/components/Taipy/Login.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { ChangeEvent, KeyboardEvent, MouseEvent, useCallback, useEffect, useState } from "react";
+import React, { ChangeEvent, KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
 import Button from "@mui/material/Button";
 import Button from "@mui/material/Button";
 import CircularProgress from "@mui/material/CircularProgress";
 import CircularProgress from "@mui/material/CircularProgress";
 import DialogTitle from "@mui/material/DialogTitle";
 import DialogTitle from "@mui/material/DialogTitle";
@@ -19,10 +19,7 @@ import Dialog from "@mui/material/Dialog";
 import DialogActions from "@mui/material/DialogActions";
 import DialogActions from "@mui/material/DialogActions";
 import DialogContent from "@mui/material/DialogContent";
 import DialogContent from "@mui/material/DialogContent";
 import DialogContentText from "@mui/material/DialogContentText";
 import DialogContentText from "@mui/material/DialogContentText";
-import FormControl from "@mui/material/FormControl";
 import InputAdornment from "@mui/material/InputAdornment";
 import InputAdornment from "@mui/material/InputAdornment";
-import InputLabel from "@mui/material/InputLabel";
-import OutlinedInput from "@mui/material/OutlinedInput";
 import TextField from "@mui/material/TextField";
 import TextField from "@mui/material/TextField";
 import IconButton from "@mui/material/IconButton";
 import IconButton from "@mui/material/IconButton";
 import CloseIcon from "@mui/icons-material/Close";
 import CloseIcon from "@mui/icons-material/Close";
@@ -51,6 +48,8 @@ const closeSx: SxProps<Theme> = {
     alignSelf: "start",
     alignSelf: "start",
 };
 };
 const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" };
 const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" };
+const userProps = { autocomplete: "username" };
+const pwdProps = { autocomplete: "current-password" };
 
 
 const Login = (props: LoginProps) => {
 const Login = (props: LoginProps) => {
     const { id, title = "Log-in", onAction = "on_login", message, defaultMessage } = props;
     const { id, title = "Log-in", onAction = "on_login", message, defaultMessage } = props;
@@ -81,13 +80,6 @@ const Login = (props: LoginProps) => {
         input == "user" ? setUser(evt.currentTarget.value) : setPassword(evt.currentTarget.value);
         input == "user" ? setUser(evt.currentTarget.value) : setPassword(evt.currentTarget.value);
     }, []);
     }, []);
 
 
-    const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []);
-
-    const handleMouseDownPassword = useCallback(
-        (event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault(),
-        []
-    );
-
     const handleEnter = useCallback(
     const handleEnter = useCallback(
         (evt: KeyboardEvent<HTMLInputElement>) => {
         (evt: KeyboardEvent<HTMLInputElement>) => {
             if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && evt.key == "Enter") {
             if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && evt.key == "Enter") {
@@ -98,6 +90,30 @@ const Login = (props: LoginProps) => {
         [handleAction]
         [handleAction]
     );
     );
 
 
+    // password
+    const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []);
+    const handleMouseDownPassword = useCallback(
+        (event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault(),
+        []
+    );
+    const passwordProps = useMemo(
+        () => ({
+            endAdornment: (
+                <InputAdornment position="end">
+                    <IconButton
+                        aria-label="toggle password visibility"
+                        onClick={handleClickShowPassword}
+                        onMouseDown={handleMouseDownPassword}
+                        edge="end"
+                    >
+                        {showPassword ? <VisibilityOff /> : <Visibility />}
+                    </IconButton>
+                </InputAdornment>
+            ),
+        }),
+        [showPassword, handleClickShowPassword, handleMouseDownPassword]
+    );
+
     useEffect(() => {
     useEffect(() => {
         nbLogins++;
         nbLogins++;
         if (nbLogins === 1) {
         if (nbLogins === 1) {
@@ -129,30 +145,23 @@ const Login = (props: LoginProps) => {
                     onChange={changeInput}
                     onChange={changeInput}
                     data-input="user"
                     data-input="user"
                     onKeyDown={handleEnter}
                     onKeyDown={handleEnter}
+                    inputProps={userProps}
                 ></TextField>
                 ></TextField>
-                <FormControl variant="outlined" data-input="password" required>
-                    <InputLabel htmlFor="taipy-login-password">Password</InputLabel>
-                    <OutlinedInput
-                        id="taipy-login-password"
-                        type={showPassword ? "text" : "password"}
-                        value={password}
-                        onChange={changeInput}
-                        endAdornment={
-                            <InputAdornment position="end">
-                                <IconButton
-                                    aria-label="toggle password visibility"
-                                    onClick={handleClickShowPassword}
-                                    onMouseDown={handleMouseDownPassword}
-                                    edge="end"
-                                >
-                                    {showPassword ? <VisibilityOff /> : <Visibility />}
-                                </IconButton>
-                            </InputAdornment>
-                        }
-                        label="Password"
-                        onKeyDown={handleEnter}
-                    />
-                </FormControl>
+                <TextField
+                    variant="outlined"
+                    label="Password"
+                    required
+                    fullWidth
+                    margin="dense"
+                    className={getSuffixedClassNames(className, "-password")}
+                    type={showPassword ? "text" : "password"}
+                    value={password}
+                    onChange={changeInput}
+                    data-input="password"
+                    onKeyDown={handleEnter}
+                    inputProps={pwdProps}
+                    InputProps={passwordProps}
+                />
                 <DialogContentText>{message || defaultMessage}</DialogContentText>
                 <DialogContentText>{message || defaultMessage}</DialogContentText>
             </DialogContent>
             </DialogContent>
             <DialogActions>
             <DialogActions>

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

@@ -173,7 +173,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                 }
                 }
                 return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
                 return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
             } catch (e) {
-                console.info("PTable.columns: " + ((e as Error).message || e));
+                console.info("PaginatedTable.columns: ", (e as Error).message || e);
             }
             }
         }
         }
         return [
         return [

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

@@ -48,6 +48,14 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai
     type: string;
     type: string;
     value: string;
     value: string;
     defaultValue?: string;
     defaultValue?: string;
+    step?: number;
+    defaultStep?: number;
+    stepMultiplier?: number;
+    defaultStepMultiplier?: number;
+    min?: number;
+    defaultMin?: number;
+    max?: number;
+    defaultMax?: number;
     changeDelay?: number;
     changeDelay?: number;
     onAction?: string;
     onAction?: string;
     actionKeys?: string;
     actionKeys?: string;
@@ -59,6 +67,15 @@ export interface TaipyLabelProps {
     label?: string;
     label?: string;
 }
 }
 
 
+export interface DateProps {
+    maxDate?: unknown;
+    maxDateTime?: unknown;
+    maxTime?: unknown;
+    minDate?: unknown;
+    minDateTime?: unknown;
+    minTime?: unknown;
+}
+
 export const getArrayValue = <T>(arr: T[], idx: number, defVal?: T): T | undefined => {
 export const getArrayValue = <T>(arr: T[], idx: number, defVal?: T): T | undefined => {
     const val = Array.isArray(arr) && idx < arr.length ? arr[idx] : undefined;
     const val = Array.isArray(arr) && idx < arr.length ? arr[idx] : undefined;
     return val ?? defVal;
     return val ?? defVal;
@@ -113,3 +130,20 @@ export const getSuffixedClassNames = (names: string | undefined, suffix: string)
 export const emptyStyle = {} as CSSProperties;
 export const emptyStyle = {} as CSSProperties;
 
 
 export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
 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 => {
+    if (!val) {
+        return {};
+    }
+    const propName: keyof DateProps = withTime
+        ? start
+            ? "minDateTime"
+            : "maxDateTime"
+        : start
+            ? "minDate"
+            : "maxDate";
+    if (p[propName] == val) {
+        return p;
+    }
+    return {...p, [propName]: val};
+};

+ 2 - 12
frontend/taipy-gui/src/utils/index.ts

@@ -170,23 +170,13 @@ export const formatWSValue = (
             if (value == "") {
             if (value == "") {
                 return "";
                 return "";
             }
             }
-            try {
-                return getDateTimeString(value.toString(), dataFormat, formatConf);
-            } catch (e) {
-                console.error(`wrong dateformat "${dataFormat || formatConf.dateTime}"`, e);
-            }
-            return getDateTimeString(value.toString(), undefined, formatConf);
+            return getDateTimeString(value.toString(), dataFormat, formatConf);
         case "datetime.date":
         case "datetime.date":
         case "date":
         case "date":
             if (value == "") {
             if (value == "") {
                 return "";
                 return "";
             }
             }
-            try {
-                return getDateTimeString(value.toString(), dataFormat, formatConf, undefined, false);
-            } catch (e) {
-                console.error(`wrong dateformat "${dataFormat || formatConf.date}"`, e);
-            }
-            return getDateTimeString(value.toString(), undefined, formatConf, undefined, false);
+            return getDateTimeString(value.toString(), dataFormat, formatConf, undefined, false);
         case "int":
         case "int":
         case "float":
         case "float":
         case "number":
         case "number":

+ 150 - 134
frontend/taipy/package-lock.json

@@ -32,7 +32,7 @@
         "eslint": "^8.20.0",
         "eslint": "^8.20.0",
         "eslint-plugin-react": "^7.30.1",
         "eslint-plugin-react": "^7.30.1",
         "eslint-plugin-react-hooks": "^4.6.0",
         "eslint-plugin-react-hooks": "^4.6.0",
-        "eslint-plugin-tsdoc": "^0.2.17",
+        "eslint-plugin-tsdoc": "^0.3.0",
         "eslint-webpack-plugin": "^4.0.0",
         "eslint-webpack-plugin": "^4.0.0",
         "ts-loader": "^9.3.1",
         "ts-loader": "^9.3.1",
         "typescript": "^5.0.2",
         "typescript": "^5.0.2",
@@ -41,6 +41,7 @@
       }
       }
     },
     },
     "../../taipy/gui/webapp": {
     "../../taipy/gui/webapp": {
+      "name": "taipy-gui",
       "version": "3.2.0"
       "version": "3.2.0"
     },
     },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {
@@ -382,9 +383,9 @@
       }
       }
     },
     },
     "node_modules/@eslint-community/regexpp": {
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
-      "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
+      "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -460,26 +461,26 @@
       }
       }
     },
     },
     "node_modules/@floating-ui/core": {
     "node_modules/@floating-ui/core": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz",
-      "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==",
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
+      "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
       "dependencies": {
       "dependencies": {
-        "@floating-ui/utils": "^0.2.0"
+        "@floating-ui/utils": "^0.2.4"
       }
       }
     },
     },
     "node_modules/@floating-ui/dom": {
     "node_modules/@floating-ui/dom": {
-      "version": "1.6.5",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz",
-      "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==",
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
+      "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
       "dependencies": {
       "dependencies": {
-        "@floating-ui/core": "^1.0.0",
-        "@floating-ui/utils": "^0.2.0"
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.4"
       }
       }
     },
     },
     "node_modules/@floating-ui/react-dom": {
     "node_modules/@floating-ui/react-dom": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz",
-      "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
+      "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
       "dependencies": {
       "dependencies": {
         "@floating-ui/dom": "^1.0.0"
         "@floating-ui/dom": "^1.0.0"
       },
       },
@@ -489,9 +490,9 @@
       }
       }
     },
     },
     "node_modules/@floating-ui/utils": {
     "node_modules/@floating-ui/utils": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
-      "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
+      "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA=="
     },
     },
     "node_modules/@humanwhocodes/config-array": {
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.14",
       "version": "0.11.14",
@@ -703,36 +704,45 @@
       }
       }
     },
     },
     "node_modules/@microsoft/tsdoc": {
     "node_modules/@microsoft/tsdoc": {
-      "version": "0.14.2",
-      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
-      "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==",
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz",
+      "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/@microsoft/tsdoc-config": {
     "node_modules/@microsoft/tsdoc-config": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz",
-      "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==",
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz",
+      "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@microsoft/tsdoc": "0.14.2",
-        "ajv": "~6.12.6",
+        "@microsoft/tsdoc": "0.15.0",
+        "ajv": "~8.12.0",
         "jju": "~1.4.0",
         "jju": "~1.4.0",
-        "resolve": "~1.19.0"
+        "resolve": "~1.22.2"
       }
       }
     },
     },
-    "node_modules/@microsoft/tsdoc-config/node_modules/resolve": {
-      "version": "1.19.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
-      "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
+    "node_modules/@microsoft/tsdoc-config/node_modules/ajv": {
+      "version": "8.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "is-core-module": "^2.1.0",
-        "path-parse": "^1.0.6"
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
       },
       },
       "funding": {
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
       }
       }
     },
     },
+    "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true
+    },
     "node_modules/@mui/base": {
     "node_modules/@mui/base": {
       "version": "5.0.0-beta.40",
       "version": "5.0.0-beta.40",
       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
       "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
@@ -765,18 +775,18 @@
       }
       }
     },
     },
     "node_modules/@mui/core-downloads-tracker": {
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.15.20",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.20.tgz",
-      "integrity": "sha512-DoL2ppgldL16utL8nNyj/P12f8mCNdx/Hb/AJnX9rLY4b52hCMIx1kH83pbXQ6uMy6n54M3StmEbvSGoj2OFuA==",
+      "version": "5.15.21",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.21.tgz",
+      "integrity": "sha512-dp9lXBaJZzJYeJfQY3Ow4Rb49QaCEdkl2KKYscdQHQm6bMJ+l4XPY3Cd9PCeeJTsHPIDJ60lzXbeRgs6sx/rpw==",
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
         "url": "https://opencollective.com/mui-org"
       }
       }
     },
     },
     "node_modules/@mui/icons-material": {
     "node_modules/@mui/icons-material": {
-      "version": "5.15.20",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz",
-      "integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==",
+      "version": "5.15.21",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.21.tgz",
+      "integrity": "sha512-yqkq1MbdkmX5ZHyvZTBuAaA6RkvoqkoAgwBSx9Oh0L0jAfj9T/Ih/NhMNjkl8PWVSonjfDUkKroBnjRyo/1M9Q==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.23.9"
         "@babel/runtime": "^7.23.9"
       },
       },
@@ -799,13 +809,13 @@
       }
       }
     },
     },
     "node_modules/@mui/material": {
     "node_modules/@mui/material": {
-      "version": "5.15.20",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz",
-      "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==",
+      "version": "5.15.21",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.21.tgz",
+      "integrity": "sha512-nTyCcgduKwHqiuQ/B03EQUa+utSMzn2sQp0QAibsnYe4tvc3zkMbO0amKpl48vhABIY3IvT6w9615BFIgMt0YA==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.23.9",
         "@babel/runtime": "^7.23.9",
         "@mui/base": "5.0.0-beta.40",
         "@mui/base": "5.0.0-beta.40",
-        "@mui/core-downloads-tracker": "^5.15.20",
+        "@mui/core-downloads-tracker": "^5.15.21",
         "@mui/system": "^5.15.20",
         "@mui/system": "^5.15.20",
         "@mui/types": "^7.2.14",
         "@mui/types": "^7.2.14",
         "@mui/utils": "^5.15.20",
         "@mui/utils": "^5.15.20",
@@ -979,14 +989,14 @@
       }
       }
     },
     },
     "node_modules/@mui/x-date-pickers": {
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.7.0.tgz",
-      "integrity": "sha512-huyoA22Vi8iCkee6ro0sX7CcFIcPV/Fl7ZGWwaQC8PTAheXhz823DjMYAiwRU/imF+UFYfUInWQ4XZCIkM+2Dw==",
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.8.0.tgz",
+      "integrity": "sha512-SkolW0vZ4XiUeD5FBevG9NQ3pAgfNYlJA5XFhSLFD/swNQRO4EYOUXw38O/ccOh1lkAcwVR+rrGPCoT4/0YGEg==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.7",
         "@babel/runtime": "^7.24.7",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
-        "@mui/system": "^5.15.15",
-        "@mui/utils": "^5.15.14",
+        "@mui/system": "^5.15.20",
+        "@mui/utils": "^5.15.20",
         "@types/react-transition-group": "^4.4.10",
         "@types/react-transition-group": "^4.4.10",
         "clsx": "^2.1.1",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "prop-types": "^15.8.1",
@@ -1044,14 +1054,14 @@
       }
       }
     },
     },
     "node_modules/@mui/x-tree-view": {
     "node_modules/@mui/x-tree-view": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.7.0.tgz",
-      "integrity": "sha512-kUTMS77EcNjp1iXZlm4GGFzZHnQdZJfn2L9gvxAaHtNTDSRMS61jpsCcXknIyC797dmRPdALPewNzSOfkThF+Q==",
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.8.0.tgz",
+      "integrity": "sha512-+Kc4SSWNFe53ozCXprizNcRIUiYc/iBceFMJZJJcOQGqsQVnH3Y7uUx2dUgO4AMp4EQR3zUW+bjE8fqhBQcK9Q==",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.7",
         "@babel/runtime": "^7.24.7",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
-        "@mui/system": "^5.15.15",
-        "@mui/utils": "^5.15.14",
+        "@mui/system": "^5.15.20",
+        "@mui/utils": "^5.15.20",
         "@types/react-transition-group": "^4.4.10",
         "@types/react-transition-group": "^4.4.10",
         "clsx": "^2.1.1",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
         "prop-types": "^15.8.1",
@@ -1262,9 +1272,9 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@types/node": {
     "node_modules/@types/node": {
-      "version": "20.14.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.5.tgz",
-      "integrity": "sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==",
+      "version": "20.14.9",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
+      "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "undici-types": "~5.26.4"
         "undici-types": "~5.26.4"
@@ -1313,16 +1323,16 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz",
-      "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
+      "integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "7.13.1",
-        "@typescript-eslint/type-utils": "7.13.1",
-        "@typescript-eslint/utils": "7.13.1",
-        "@typescript-eslint/visitor-keys": "7.13.1",
+        "@typescript-eslint/scope-manager": "7.15.0",
+        "@typescript-eslint/type-utils": "7.15.0",
+        "@typescript-eslint/utils": "7.15.0",
+        "@typescript-eslint/visitor-keys": "7.15.0",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
         "natural-compare": "^1.4.0",
@@ -1346,15 +1356,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz",
-      "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
+      "integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.13.1",
-        "@typescript-eslint/types": "7.13.1",
-        "@typescript-eslint/typescript-estree": "7.13.1",
-        "@typescript-eslint/visitor-keys": "7.13.1",
+        "@typescript-eslint/scope-manager": "7.15.0",
+        "@typescript-eslint/types": "7.15.0",
+        "@typescript-eslint/typescript-estree": "7.15.0",
+        "@typescript-eslint/visitor-keys": "7.15.0",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
@@ -1374,13 +1384,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz",
-      "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
+      "integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.13.1",
-        "@typescript-eslint/visitor-keys": "7.13.1"
+        "@typescript-eslint/types": "7.15.0",
+        "@typescript-eslint/visitor-keys": "7.15.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1391,13 +1401,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz",
-      "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
+      "integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.13.1",
-        "@typescript-eslint/utils": "7.13.1",
+        "@typescript-eslint/typescript-estree": "7.15.0",
+        "@typescript-eslint/utils": "7.15.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
         "ts-api-utils": "^1.3.0"
       },
       },
@@ -1418,9 +1428,9 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz",
-      "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
+      "integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1431,13 +1441,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree": {
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz",
-      "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
+      "integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.13.1",
-        "@typescript-eslint/visitor-keys": "7.13.1",
+        "@typescript-eslint/types": "7.15.0",
+        "@typescript-eslint/visitor-keys": "7.15.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
@@ -1459,15 +1469,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/utils": {
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz",
-      "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
+      "integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "7.13.1",
-        "@typescript-eslint/types": "7.13.1",
-        "@typescript-eslint/typescript-estree": "7.13.1"
+        "@typescript-eslint/scope-manager": "7.15.0",
+        "@typescript-eslint/types": "7.15.0",
+        "@typescript-eslint/typescript-estree": "7.15.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1481,12 +1491,12 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/visitor-keys": {
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.13.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz",
-      "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==",
+      "version": "7.15.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
+      "integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.13.1",
+        "@typescript-eslint/types": "7.15.0",
         "eslint-visitor-keys": "^3.4.3"
         "eslint-visitor-keys": "^3.4.3"
       },
       },
       "engines": {
       "engines": {
@@ -2098,9 +2108,9 @@
       }
       }
     },
     },
     "node_modules/caniuse-lite": {
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001636",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
-      "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
+      "version": "1.0.30001639",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz",
+      "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==",
       "dev": true,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -2439,9 +2449,9 @@
       }
       }
     },
     },
     "node_modules/electron-to-chromium": {
     "node_modules/electron-to-chromium": {
-      "version": "1.4.805",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.805.tgz",
-      "integrity": "sha512-8W4UJwX/w9T0QSzINJckTKG6CYpAUTqsaWcWIsdud3I1FYJcMgW9QqT1/4CBff/pP/TihWh13OmiyY8neto6vw==",
+      "version": "1.4.816",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz",
+      "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/enhanced-resolve": {
     "node_modules/enhanced-resolve": {
@@ -2584,9 +2594,9 @@
       }
       }
     },
     },
     "node_modules/es-module-lexer": {
     "node_modules/es-module-lexer": {
-      "version": "1.5.3",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz",
-      "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==",
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
+      "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/es-object-atoms": {
     "node_modules/es-object-atoms": {
@@ -2717,16 +2727,16 @@
       }
       }
     },
     },
     "node_modules/eslint-plugin-react": {
     "node_modules/eslint-plugin-react": {
-      "version": "7.34.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz",
-      "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==",
+      "version": "7.34.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz",
+      "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "array-includes": "^3.1.8",
         "array-includes": "^3.1.8",
         "array.prototype.findlast": "^1.2.5",
         "array.prototype.findlast": "^1.2.5",
         "array.prototype.flatmap": "^1.3.2",
         "array.prototype.flatmap": "^1.3.2",
         "array.prototype.toreversed": "^1.1.2",
         "array.prototype.toreversed": "^1.1.2",
-        "array.prototype.tosorted": "^1.1.3",
+        "array.prototype.tosorted": "^1.1.4",
         "doctrine": "^2.1.0",
         "doctrine": "^2.1.0",
         "es-iterator-helpers": "^1.0.19",
         "es-iterator-helpers": "^1.0.19",
         "estraverse": "^5.3.0",
         "estraverse": "^5.3.0",
@@ -2821,13 +2831,13 @@
       }
       }
     },
     },
     "node_modules/eslint-plugin-tsdoc": {
     "node_modules/eslint-plugin-tsdoc": {
-      "version": "0.2.17",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz",
-      "integrity": "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz",
+      "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@microsoft/tsdoc": "0.14.2",
-        "@microsoft/tsdoc-config": "0.16.2"
+        "@microsoft/tsdoc": "0.15.0",
+        "@microsoft/tsdoc-config": "0.17.0"
       }
       }
     },
     },
     "node_modules/eslint-scope": {
     "node_modules/eslint-scope": {
@@ -3707,11 +3717,14 @@
       }
       }
     },
     },
     "node_modules/is-core-module": {
     "node_modules/is-core-module": {
-      "version": "2.13.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
-      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz",
+      "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==",
       "dependencies": {
       "dependencies": {
-        "hasown": "^2.0.0"
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       },
       "funding": {
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
@@ -4361,9 +4374,9 @@
       }
       }
     },
     },
     "node_modules/minimatch": {
     "node_modules/minimatch": {
-      "version": "9.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
-      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "brace-expansion": "^2.0.1"
         "brace-expansion": "^2.0.1"
@@ -4416,10 +4429,13 @@
       }
       }
     },
     },
     "node_modules/object-inspect": {
     "node_modules/object-inspect": {
-      "version": "1.13.1",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
-      "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+      "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
       "dev": true,
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
       }
       }
@@ -5778,9 +5794,9 @@
       }
       }
     },
     },
     "node_modules/typescript": {
     "node_modules/typescript": {
-      "version": "5.4.5",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
-      "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+      "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
       "dev": true,
       "dev": true,
       "bin": {
       "bin": {
         "tsc": "bin/tsc",
         "tsc": "bin/tsc",
@@ -5864,9 +5880,9 @@
       }
       }
     },
     },
     "node_modules/webpack": {
     "node_modules/webpack": {
-      "version": "5.92.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.0.tgz",
-      "integrity": "sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==",
+      "version": "5.92.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
+      "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@types/eslint-scope": "^3.7.3",
         "@types/eslint-scope": "^3.7.3",

+ 1 - 1
frontend/taipy/package.json

@@ -11,7 +11,7 @@
     "eslint": "^8.20.0",
     "eslint": "^8.20.0",
     "eslint-plugin-react": "^7.30.1",
     "eslint-plugin-react": "^7.30.1",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-hooks": "^4.6.0",
-    "eslint-plugin-tsdoc": "^0.2.17",
+    "eslint-plugin-tsdoc": "^0.3.0",
     "eslint-webpack-plugin": "^4.0.0",
     "eslint-webpack-plugin": "^4.0.0",
     "ts-loader": "^9.3.1",
     "ts-loader": "^9.3.1",
     "typescript": "^5.0.2",
     "typescript": "^5.0.2",

+ 26 - 11
frontend/taipy/src/ScenarioDag.tsx

@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { Point } from '@projectstorm/geometry';
+import React, { useCallback, useEffect, useMemo, useState, useRef } from "react";
+import { Point } from "@projectstorm/geometry";
 import { CanvasWidget } from "@projectstorm/react-canvas-core";
 import { CanvasWidget } from "@projectstorm/react-canvas-core";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import AppBar from "@mui/material/AppBar";
 import AppBar from "@mui/material/AppBar";
@@ -76,6 +76,8 @@ const getValidScenario = (scenar: DisplayModel | DisplayModel[]) =>
         ? (scenar[0] as DisplayModel)
         ? (scenar[0] as DisplayModel)
         : undefined;
         : undefined;
 
 
+const preventWheel = (e: Event) => e.preventDefault();
+
 const ScenarioDag = (props: ScenarioDagProps) => {
 const ScenarioDag = (props: ScenarioDagProps) => {
     const { showToolbar = true, onSelect, onAction } = props;
     const { showToolbar = true, onSelect, onAction } = props;
     const [scenarioId, setScenarioId] = useState("");
     const [scenarioId, setScenarioId] = useState("");
@@ -85,6 +87,7 @@ const ScenarioDag = (props: ScenarioDagProps) => {
     const [taskStatuses, setTaskStatuses] = useState<TaskStatuses>();
     const [taskStatuses, setTaskStatuses] = useState<TaskStatuses>();
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
+    const canvasRef = useRef<CanvasWidget>(null);
 
 
     const render = useDynamicProperty(props.render, props.defaultRender, true);
     const render = useDynamicProperty(props.render, props.defaultRender, true);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
@@ -144,16 +147,18 @@ const ScenarioDag = (props: ScenarioDagProps) => {
             // populate model
             // populate model
             doLayout = populateModel(addStatusToDisplayModel(displayModel, taskStatuses), model);
             doLayout = populateModel(addStatusToDisplayModel(displayModel, taskStatuses), model);
         }
         }
-        const rects = engine.getModel() && engine
-            .getModel()
-            .getNodes()
-            .reduce((pv, nm) => {
-                pv[nm.getID()] = nm.getPosition();
-                return pv;
-            }, {} as Record<string, Point>);
+        const rects =
+            engine.getModel() &&
+            engine
+                .getModel()
+                .getNodes()
+                .reduce((pv, nm) => {
+                    pv[nm.getID()] = nm.getPosition();
+                    return pv;
+                }, {} as Record<string, Point>);
         const hasPos = rects && Object.keys(rects).length;
         const hasPos = rects && Object.keys(rects).length;
         if (hasPos) {
         if (hasPos) {
-            model.getNodes().forEach(nm => rects[nm.getID()] && nm.setPosition(rects[nm.getID()]));
+            model.getNodes().forEach((nm) => rects[nm.getID()] && nm.setPosition(rects[nm.getID()]));
         }
         }
         engine.setModel(model);
         engine.setModel(model);
         model.setLocked(true);
         model.setLocked(true);
@@ -165,10 +170,20 @@ const ScenarioDag = (props: ScenarioDagProps) => {
         showVar && dispatch(createSendUpdateAction(showVar, render, module));
         showVar && dispatch(createSendUpdateAction(showVar, render, module));
     }, [render, props.updateVars, dispatch, module]);
     }, [render, props.updateVars, dispatch, module]);
 
 
+    useEffect(() => {
+        setTimeout(() => {
+            // wait for div to be referenced and then set a non passive listener on wheel event
+            canvasRef.current?.ref.current?.addEventListener("wheel", preventWheel, { passive: false });
+        }, 300);
+        // remove the listener
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+        return () => canvasRef.current?.ref.current?.removeEventListener("wheel", preventWheel);
+    }, []);
+
     return render && scenarioId ? (
     return render && scenarioId ? (
         <Paper sx={sizeSx} id={props.id} className={className}>
         <Paper sx={sizeSx} id={props.id} className={className}>
             {showToolbar ? <DagTitle zoomToFit={zoomToFit} /> : null}
             {showToolbar ? <DagTitle zoomToFit={zoomToFit} /> : null}
-            <CanvasWidget engine={engine} />
+            <CanvasWidget engine={engine} ref={canvasRef} />
         </Paper>
         </Paper>
     ) : null;
     ) : null;
 };
 };

+ 6 - 6
taipy/core/_entity/_ready_to_run_property.py

@@ -12,7 +12,7 @@
 from typing import TYPE_CHECKING, Dict, Set, Union
 from typing import TYPE_CHECKING, Dict, Set, Union
 
 
 from ..notification import EventOperation, Notifier, _make_event
 from ..notification import EventOperation, Notifier, _make_event
-from ..reason.reason import Reasons
+from ..reason import Reason, ReasonCollection
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from ..data.data_node import DataNode, DataNodeId
     from ..data.data_node import DataNode, DataNodeId
@@ -29,10 +29,10 @@ class _ReadyToRunProperty:
 
 
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # the data nodes that make it not ready_to_run with the reason(s)
     # the data nodes that make it not ready_to_run with the reason(s)
-    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Reasons] = {}
+    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], ReasonCollection] = {}
 
 
     @classmethod
     @classmethod
-    def _add(cls, dn: "DataNode", reason: str) -> None:
+    def _add(cls, dn: "DataNode", reason: Reason) -> None:
         from ..scenario.scenario import Scenario
         from ..scenario.scenario import Scenario
         from ..sequence.sequence import Sequence
         from ..sequence.sequence import Sequence
         from ..task.task import Task
         from ..task.task import Task
@@ -50,7 +50,7 @@ class _ReadyToRunProperty:
                 cls.__add(task_parent, dn, reason)
                 cls.__add(task_parent, dn, reason)
 
 
     @classmethod
     @classmethod
-    def _remove(cls, datanode: "DataNode", reason: str) -> None:
+    def _remove(cls, datanode: "DataNode", reason: Reason) -> None:
         from ..taipy import get as tp_get
         from ..taipy import get as tp_get
 
 
         # check the data node status to determine the reason to be removed
         # check the data node status to determine the reason to be removed
@@ -72,7 +72,7 @@ class _ReadyToRunProperty:
             cls._datanode_id_submittables.pop(datanode.id)
             cls._datanode_id_submittables.pop(datanode.id)
 
 
     @classmethod
     @classmethod
-    def __add(cls, submittable: Union["Scenario", "Sequence", "Task"], datanode: "DataNode", reason: str) -> None:
+    def __add(cls, submittable: Union["Scenario", "Sequence", "Task"], datanode: "DataNode", reason: Reason) -> None:
         if datanode.id not in cls._datanode_id_submittables:
         if datanode.id not in cls._datanode_id_submittables:
             cls._datanode_id_submittables[datanode.id] = set()
             cls._datanode_id_submittables[datanode.id] = set()
         cls._datanode_id_submittables[datanode.id].add(submittable.id)
         cls._datanode_id_submittables[datanode.id].add(submittable.id)
@@ -81,7 +81,7 @@ class _ReadyToRunProperty:
             cls.__publish_submittable_property_event(submittable, False)
             cls.__publish_submittable_property_event(submittable, False)
 
 
         if submittable.id not in cls._submittable_id_datanodes:
         if submittable.id not in cls._submittable_id_datanodes:
-            cls._submittable_id_datanodes[submittable.id] = Reasons(submittable.id)
+            cls._submittable_id_datanodes[submittable.id] = ReasonCollection()
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
 
 
     @staticmethod
     @staticmethod

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

@@ -19,8 +19,7 @@ from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
 from ..data.data_node import DataNode
 from ..data.data_node import DataNode
 from ..job.job import Job
 from ..job.job import Job
-from ..reason._reason_factory import _build_data_node_is_being_edited_reason, _build_data_node_is_not_written
-from ..reason.reason import Reasons
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten, ReasonCollection
 from ..submission.submission import Submission
 from ..submission.submission import Submission
 from ..task.task import Task
 from ..task.task import Task
 from ._dag import _DAG
 from ._dag import _DAG
@@ -83,22 +82,22 @@ class Submittable:
         all_data_nodes_in_dag = {node for node in dag.nodes if isinstance(node, DataNode)}
         all_data_nodes_in_dag = {node for node in dag.nodes if isinstance(node, DataNode)}
         return all_data_nodes_in_dag - self.__get_inputs(dag) - self.__get_outputs(dag)
         return all_data_nodes_in_dag - self.__get_inputs(dag) - self.__get_outputs(dag)
 
 
-    def is_ready_to_run(self) -> Reasons:
+    def is_ready_to_run(self) -> ReasonCollection:
         """Indicate if the entity is ready to be run.
         """Indicate if the entity is ready to be run.
 
 
         Returns:
         Returns:
             A Reason object that can function as a Boolean value.
             A Reason object that can function as a Boolean value.
             which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
             which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
         """
         """
-        reason = Reasons(self._submittable_id)
+        reason_collection = ReasonCollection()
 
 
         for node in self.get_inputs():
         for node in self.get_inputs():
             if node._edit_in_progress:
             if node._edit_in_progress:
-                reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
+                reason_collection._add_reason(node.id, DataNodeEditInProgress(node.id))
             if not node._last_edit_date:
             if not node._last_edit_date:
-                reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
+                reason_collection._add_reason(node.id, DataNodeIsNotWritten(node.id))
 
 
-        return reason
+        return reason_collection
 
 
     def data_nodes_being_edited(self) -> Set[DataNode]:
     def data_nodes_being_edited(self) -> Set[DataNode]:
         """Return the set of data nodes of the submittable entity that are being edited.
         """Return the set of data nodes of the submittable entity that are being edited.

+ 6 - 7
taipy/core/data/_data_manager.py

@@ -22,8 +22,7 @@ from ..config.data_node_config import DataNodeConfig
 from ..cycle.cycle_id import CycleId
 from ..cycle.cycle_id import CycleId
 from ..exceptions.exceptions import InvalidDataNodeType
 from ..exceptions.exceptions import InvalidDataNodeType
 from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import _build_not_global_scope_reason, _build_wrong_config_type_reason
-from ..reason.reason import Reasons
+from ..reason import NotGlobalScope, ReasonCollection, WrongConfigType
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..sequence.sequence_id import SequenceId
 from ._data_fs_repository import _DataFSRepository
 from ._data_fs_repository import _DataFSRepository
@@ -69,17 +68,17 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         }
         }
 
 
     @classmethod
     @classmethod
-    def _can_create(cls, config: Optional[DataNodeConfig] = None) -> Reasons:
+    def _can_create(cls, config: Optional[DataNodeConfig] = None) -> ReasonCollection:
         config_id = getattr(config, "id", None) or str(config)
         config_id = getattr(config, "id", None) or str(config)
-        reason = Reasons(config_id)
+        reason_collection = ReasonCollection()
 
 
         if config is not None:
         if config is not None:
             if not isinstance(config, DataNodeConfig):
             if not isinstance(config, DataNodeConfig):
-                reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "DataNodeConfig"))
+                reason_collection._add_reason(config_id, WrongConfigType(config_id, DataNodeConfig.__name__))
             elif config.scope is not Scope.GLOBAL:
             elif config.scope is not Scope.GLOBAL:
-                reason._add_reason(config_id, _build_not_global_scope_reason(config_id))
+                reason_collection._add_reason(config_id, NotGlobalScope(config_id))
 
 
-        return reason
+        return reason_collection
 
 
     @classmethod
     @classmethod
     def _create_and_set(
     def _create_and_set(

+ 6 - 5
taipy/core/data/data_node.py

@@ -32,6 +32,7 @@ from ..common._warnings import _warn_deprecated
 from ..exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from ..exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from ..job.job_id import JobId
 from ..job.job_id import JobId
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten
 from ._filter import _FilterDataNode
 from ._filter import _FilterDataNode
 from .data_node_id import DataNodeId, Edit
 from .data_node_id import DataNodeId, Edit
 from .operator import JoinOperator
 from .operator import JoinOperator
@@ -43,13 +44,13 @@ def _update_ready_for_reading(fct):
     def _recompute_is_ready_for_reading(dn: "DataNode", *args, **kwargs):
     def _recompute_is_ready_for_reading(dn: "DataNode", *args, **kwargs):
         fct(dn, *args, **kwargs)
         fct(dn, *args, **kwargs)
         if dn._edit_in_progress:
         if dn._edit_in_progress:
-            _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is being edited")
+            _ReadyToRunProperty._add(dn, DataNodeEditInProgress(dn.id))
         else:
         else:
-            _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is being edited")
+            _ReadyToRunProperty._remove(dn, DataNodeEditInProgress(dn.id))
         if not dn._last_edit_date:
         if not dn._last_edit_date:
-            _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is not written")
+            _ReadyToRunProperty._add(dn, DataNodeIsNotWritten(dn.id))
         else:
         else:
-            _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is not written")
+            _ReadyToRunProperty._remove(dn, DataNodeIsNotWritten(dn.id))
 
 
     return _recompute_is_ready_for_reading
     return _recompute_is_ready_for_reading
 
 
@@ -396,7 +397,7 @@ class DataNode(_Entity, _Labeled):
             return self.read_or_raise()
             return self.read_or_raise()
         except NoData:
         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 " f"written."
+                f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             )
             return None
             return None
 
 

+ 9 - 1
taipy/core/reason/__init__.py

@@ -9,4 +9,12 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-from .reason import Reasons
+from .reason import (
+    DataNodeEditInProgress,
+    DataNodeIsNotWritten,
+    EntityIsNotSubmittableEntity,
+    NotGlobalScope,
+    Reason,
+    WrongConfigType,
+)
+from .reason_collection import ReasonCollection

+ 0 - 37
taipy/core/reason/_reason_factory.py

@@ -1,37 +0,0 @@
-# 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.
-
-from typing import Optional
-
-from ..data.data_node import DataNodeId
-
-
-def _build_data_node_is_being_edited_reason(dn_id: DataNodeId) -> str:
-    return f"DataNode {dn_id} is being edited"
-
-
-def _build_data_node_is_not_written(dn_id: DataNodeId) -> str:
-    return f"DataNode {dn_id} is not written"
-
-
-def _build_not_submittable_entity_reason(entity_id: str) -> str:
-    return f"Entity {entity_id} is not a submittable entity"
-
-
-def _build_wrong_config_type_reason(config_id: str, config_type: Optional[str]) -> str:
-    if config_type:
-        return f'Object "{config_id}" must be a valid {config_type}'
-
-    return f'Object "{config_id}" is not a valid config to be created'
-
-
-def _build_not_global_scope_reason(config_id: str) -> str:
-    return f'Data node config "{config_id}" does not have GLOBAL scope'

+ 106 - 22
taipy/core/reason/reason.py

@@ -9,33 +9,117 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-from typing import Dict, Set
+from typing import Any, Optional
 
 
 
 
-class Reasons:
-    def __init__(self, entity_id: str) -> None:
-        self.entity_id: str = entity_id
-        self._reasons: Dict[str, Set[str]] = {}
+class Reason:
+    """
+    A reason explains why a specific action cannot be performed.
 
 
-    def _add_reason(self, entity_id: str, reason: str) -> "Reasons":
-        if entity_id not in self._reasons:
-            self._reasons[entity_id] = set()
-        self._reasons[entity_id].add(reason)
-        return self
+    This is a parent class aiming at being implemented by specific sub-classes.
 
 
-    def _remove_reason(self, entity_id: str, reason: str) -> "Reasons":
-        if entity_id in self._reasons and reason in self._reasons[entity_id]:
-            self._reasons[entity_id].remove(reason)
-            if len(self._reasons[entity_id]) == 0:
-                del self._reasons[entity_id]
-        return self
+    Because Taipy applications are natively multiuser, asynchronous, and dynamic,
+    some functions might not be called in some specific contexts. You can protect
+    such calls by calling other methods that return a Reasons object. It acts like a
+    boolean: True if the operation can be performed and False otherwise.
+    If the action cannot be performed, the Reasons object holds all the `reasons as a list
+    of `Reason` objects. Each `Reason` holds an explanation of why the operation cannot be
+    performed.
 
 
-    def _entity_id_exists_in_reason(self, entity_id: str) -> bool:
-        return entity_id in self._reasons
+    Attributes:
+        reason (str): The English representation of the reason why the action cannot be performed.
+    """
 
 
-    def __bool__(self) -> bool:
-        return len(self._reasons) == 0
+    def __init__(self, reason: str):
+        self._reason = reason
+
+    def __str__(self) -> str:
+        return self._reason
+
+    def __repr__(self) -> str:
+        return self._reason
+
+    def __hash__(self) -> int:
+        return hash(self._reason)
+
+    def __eq__(self, value: Any) -> bool:
+        return isinstance(value, Reason) and value._reason == self._reason
+
+
+class _DataNodeReasonMixin:
+    def __init__(self, datanode_id: str):
+        self.datanode_id = datanode_id
 
 
     @property
     @property
-    def reasons(self) -> str:
-        return "; ".join("; ".join(reason) for reason in self._reasons.values()) + "." if self._reasons else ""
+    def datanode(self):
+        from ..data._data_manager_factory import _DataManagerFactory
+
+        return _DataManagerFactory._build_manager()._get(self.datanode_id)
+
+
+class DataNodeEditInProgress(Reason, _DataNodeReasonMixin):
+    """
+    A `DataNode^` is being edited, which prevents specific actions from being performed.
+
+    Attributes:
+        datanode_id (str): The identifier of the `DataNode^`.
+    """
+
+    def __init__(self, datanode_id: str):
+        Reason.__init__(self, f"DataNode {datanode_id} is being edited")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class DataNodeIsNotWritten(Reason, _DataNodeReasonMixin):
+    """
+    A `DataNode^` has never been written, which prevents specific actions from being performed.
+
+    Attributes:
+        datanode_id (str): The identifier of the `DataNode^`.
+    """
+
+    def __init__(self, datanode_id: str):
+        Reason.__init__(self, f"DataNode {datanode_id} is not written")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class EntityIsNotSubmittableEntity(Reason):
+    """
+    An entity is not a submittable entity, which prevents specific actions from being performed.
+
+    Attributes:
+        entity_id (str): The identifier of the `Entity^`.
+    """
+
+    def __init__(self, entity_id: str):
+        Reason.__init__(self, f"Entity {entity_id} is not a submittable entity")
+
+
+class WrongConfigType(Reason):
+    """
+    A config id is not a valid expected config, which prevents specific actions from being performed.
+
+    Attributes:
+        config_id (str): The identifier of the config.
+        config_type (str): The expected config type.
+    """
+
+    def __init__(self, config_id: str, config_type: Optional[str]):
+        if config_type:
+            reason = f'Object "{config_id}" must be a valid {config_type}'
+        else:
+            reason = f'Object "{config_id}" is not a valid config to be created'
+
+        Reason.__init__(self, reason)
+
+
+class NotGlobalScope(Reason):
+    """
+    A data node config does not have a GLOBAL scope, which prevents specific actions from being performed.
+
+    Attributes:
+        config_id (str): The identifier of the config.
+    """
+
+    def __init__(self, config_id: str):
+        Reason.__init__(self, f'Data node config "{config_id}" does not have GLOBAL scope')

+ 60 - 0
taipy/core/reason/reason_collection.py

@@ -0,0 +1,60 @@
+# 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.
+
+from typing import Dict, Set
+
+from .reason import Reason
+
+
+class ReasonCollection:
+    """
+    This class is used to store all the reasons to explain why some Taipy operations are not allowed.
+
+    Because Taipy applications are natively multiuser, asynchronous, and dynamic,
+    some functions might not be called in some specific contexts. You can protect
+    such calls by calling other methods that return a `ReasonCollection`. It acts like a
+    boolean: True if the operation can be performed and False otherwise.
+    If the action cannot be performed, the ReasonCollection holds all the individual reasons as a list
+    of `Reason` objects. Each `Reason` explains why the operation cannot be performed.
+    """
+
+    def __init__(self) -> None:
+        self._reasons: Dict[str, Set[Reason]] = {}
+
+    def _add_reason(self, entity_id: str, reason: Reason) -> "ReasonCollection":
+        if entity_id not in self._reasons:
+            self._reasons[entity_id] = set()
+        self._reasons[entity_id].add(reason)
+        return self
+
+    def _remove_reason(self, entity_id: str, reason: Reason) -> "ReasonCollection":
+        if entity_id in self._reasons and reason in self._reasons[entity_id]:
+            self._reasons[entity_id].remove(reason)
+            if len(self._reasons[entity_id]) == 0:
+                del self._reasons[entity_id]
+        return self
+
+    def _entity_id_exists_in_reason(self, entity_id: str) -> bool:
+        return entity_id in self._reasons
+
+    def __bool__(self) -> bool:
+        return len(self._reasons) == 0
+
+    @property
+    def reasons(self) -> str:
+        """Retrieves a collection of reasons as a string that explains why the action cannot be performed.
+
+        Returns:
+            A string that contains all the reasons why the action cannot be performed.
+        """
+        if self._reasons:
+            return "; ".join("; ".join([str(reason) for reason in reasons]) for reasons in self._reasons.values()) + "."
+        return ""

+ 40 - 14
taipy/core/scenario/_scenario_manager.py

@@ -9,7 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-import datetime
+from datetime import datetime
 from functools import partial
 from functools import partial
 from typing import Any, Callable, Dict, List, Literal, Optional, Union
 from typing import Any, Callable, Dict, List, Literal, Optional, Union
 
 
@@ -39,8 +39,7 @@ from ..exceptions.exceptions import (
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import _build_not_submittable_entity_reason, _build_wrong_config_type_reason
-from ..reason.reason import Reasons
+from ..reason import EntityIsNotSubmittableEntity, ReasonCollection, WrongConfigType
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission.submission import Submission
 from ..submission.submission import Submission
 from ..task._task_manager_factory import _TaskManagerFactory
 from ..task._task_manager_factory import _TaskManagerFactory
@@ -108,21 +107,21 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
         )
 
 
     @classmethod
     @classmethod
-    def _can_create(cls, config: Optional[ScenarioConfig] = None) -> Reasons:
+    def _can_create(cls, config: Optional[ScenarioConfig] = None) -> ReasonCollection:
         config_id = getattr(config, "id", None) or str(config)
         config_id = getattr(config, "id", None) or str(config)
-        reason = Reasons(config_id)
+        reason_collector = ReasonCollection()
 
 
         if config is not None:
         if config is not None:
             if not isinstance(config, ScenarioConfig):
             if not isinstance(config, ScenarioConfig):
-                reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "ScenarioConfig"))
+                reason_collector._add_reason(config_id, WrongConfigType(config_id, ScenarioConfig.__name__))
 
 
-        return reason
+        return reason_collector
 
 
     @classmethod
     @classmethod
     def _create(
     def _create(
         cls,
         cls,
         config: ScenarioConfig,
         config: ScenarioConfig,
-        creation_date: Optional[datetime.datetime] = None,
+        creation_date: Optional[datetime] = None,
         name: Optional[str] = None,
         name: Optional[str] = None,
     ) -> Scenario:
     ) -> Scenario:
         _task_manager = _TaskManagerFactory._build_manager()
         _task_manager = _TaskManagerFactory._build_manager()
@@ -202,15 +201,15 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return scenario
         return scenario
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reasons:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> ReasonCollection:
         if isinstance(scenario, str):
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
             scenario = cls._get(scenario)
 
 
         if not isinstance(scenario, Scenario):
         if not isinstance(scenario, Scenario):
             scenario = str(scenario)
             scenario = str(scenario)
-            reason = Reasons((scenario))
-            reason._add_reason(scenario, _build_not_submittable_entity_reason(scenario))
-            return reason
+            reason_collector = ReasonCollection()
+            reason_collector._add_reason(scenario, EntityIsNotSubmittableEntity(scenario))
+            return reason_collector
 
 
         return scenario.is_ready_to_run()
         return scenario.is_ready_to_run()
 
 
@@ -288,9 +287,8 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
     def _get_primary_scenarios(cls) -> List[Scenario]:
     def _get_primary_scenarios(cls) -> List[Scenario]:
         return [scenario for scenario in cls._get_all() if scenario.is_primary]
         return [scenario for scenario in cls._get_all() if scenario.is_primary]
 
 
-    @classmethod
+    @staticmethod
     def _sort_scenarios(
     def _sort_scenarios(
-        cls,
         scenarios: List[Scenario],
         scenarios: List[Scenario],
         descending: bool = False,
         descending: bool = False,
         sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
         sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
@@ -306,6 +304,34 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
             scenarios.sort(key=lambda x: (x.name, x.id), reverse=descending)
             scenarios.sort(key=lambda x: (x.name, x.id), reverse=descending)
         return scenarios
         return scenarios
 
 
+    @staticmethod
+    def _filter_by_creation_time(
+        scenarios: List[Scenario],
+        created_start_time: Optional[datetime] = None,
+        created_end_time: Optional[datetime] = None,
+    ) -> List[Scenario]:
+        """
+        Filter a list of scenarios by a given creation time period.
+        The time period is inclusive.
+
+        Parameters:
+            created_start_time (Optional[datetime]): Start time of the period.
+            created_end_time (Optional[datetime]): End time of the period.
+
+        Returns:
+            List[Scenario]: List of scenarios created in the given time period.
+        """
+        if not created_start_time and not created_end_time:
+            return scenarios
+
+        if not created_start_time:
+            return [scenario for scenario in scenarios if scenario.creation_date <= created_end_time]
+
+        if not created_end_time:
+            return [scenario for scenario in scenarios if created_start_time <= scenario.creation_date]
+
+        return [scenario for scenario in scenarios if created_start_time <= scenario.creation_date <= created_end_time]
+
     @classmethod
     @classmethod
     def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
     def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
         if isinstance(scenario, str):
         if isinstance(scenario, str):

+ 6 - 3
taipy/core/scenario/scenario.py

@@ -50,9 +50,12 @@ from .scenario_id import ScenarioId
 class Scenario(_Entity, Submittable, _Labeled):
 class Scenario(_Entity, Submittable, _Labeled):
     """Instance of a Business case to solve.
     """Instance of a Business case to solve.
 
 
-    A scenario holds a set of tasks (instances of `Task^` class) to submit for execution in order to
-    solve the Business case. It also holds a set of additional data nodes (instances of `DataNode` class)
-    for extra data related to the scenario.
+    A scenario holds a set of tasks (instances of `Task^` class) to submit for execution in order
+    to solve the Business case. Each task can be connected to some data nodes as input or output
+    forming an execution graph. The scenario can be submitted for execution, and the tasks will
+    be orchestrated to solve the Business case.<br>
+    A scenario also holds a set of additional data nodes (instances of `DataNode^` class) for
+    extra data related to the scenario.
 
 
     !!! note
     !!! note
 
 

+ 5 - 6
taipy/core/sequence/_sequence_manager.py

@@ -29,8 +29,7 @@ from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification.event import _make_event
 from ..notification.event import _make_event
-from ..reason._reason_factory import _build_not_submittable_entity_reason
-from ..reason.reason import Reasons
+from ..reason import EntityIsNotSubmittableEntity, ReasonCollection
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario.scenario import Scenario
 from ..scenario.scenario import Scenario
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
@@ -342,15 +341,15 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reasons:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> ReasonCollection:
         if isinstance(sequence, str):
         if isinstance(sequence, str):
             sequence = cls._get(sequence)
             sequence = cls._get(sequence)
 
 
         if not isinstance(sequence, Sequence):
         if not isinstance(sequence, Sequence):
             sequence = str(sequence)
             sequence = str(sequence)
-            reason = Reasons(sequence)
-            reason._add_reason(sequence, _build_not_submittable_entity_reason(sequence))
-            return reason
+            reason_collector = ReasonCollection()
+            reason_collector._add_reason(sequence, EntityIsNotSubmittableEntity(sequence))
+            return reason_collector
 
 
         return sequence.is_ready_to_run()
         return sequence.is_ready_to_run()
 
 

+ 17 - 5
taipy/core/taipy.py

@@ -45,8 +45,7 @@ from .exceptions.exceptions import (
 from .job._job_manager_factory import _JobManagerFactory
 from .job._job_manager_factory import _JobManagerFactory
 from .job.job import Job
 from .job.job import Job
 from .job.job_id import JobId
 from .job.job_id import JobId
-from .reason._reason_factory import _build_not_submittable_entity_reason
-from .reason.reason import Reasons
+from .reason import EntityIsNotSubmittableEntity, ReasonCollection
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario.scenario import Scenario
 from .scenario.scenario import Scenario
 from .scenario.scenario_id import ScenarioId
 from .scenario.scenario_id import ScenarioId
@@ -85,7 +84,7 @@ def set(entity: Union[DataNode, Task, Sequence, Scenario, Cycle, Submission]):
         return _SubmissionManagerFactory._build_manager()._set(entity)
         return _SubmissionManagerFactory._build_manager()._set(entity)
 
 
 
 
-def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> Reasons:
+def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> ReasonCollection:
     """Indicate if an entity can be submitted.
     """Indicate if an entity can be submitted.
 
 
     This function checks if the given entity can be submitted for execution.
     This function checks if the given entity can be submitted for execution.
@@ -105,7 +104,7 @@ def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Tas
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
-    return Reasons(str(entity))._add_reason(str(entity), _build_not_submittable_entity_reason(str(entity)))
+    return ReasonCollection()._add_reason(str(entity), EntityIsNotSubmittableEntity(str(entity)))
 
 
 
 
 def is_editable(
 def is_editable(
@@ -510,6 +509,8 @@ def get_scenarios(
     tag: Optional[str] = None,
     tag: Optional[str] = None,
     is_sorted: bool = False,
     is_sorted: bool = False,
     descending: bool = False,
     descending: bool = False,
+    created_start_time: Optional[datetime] = None,
+    created_end_time: Optional[datetime] = None,
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
 ) -> List[Scenario]:
 ) -> List[Scenario]:
     """Retrieve a list of existing scenarios filtered by cycle or tag.
     """Retrieve a list of existing scenarios filtered by cycle or tag.
@@ -526,6 +527,8 @@ def get_scenarios(
             The default value is False.
             The default value is False.
         descending (bool): If True, sort the output list of scenarios in descending order.
         descending (bool): If True, sort the output list of scenarios in descending order.
             The default value is False.
             The default value is False.
+        created_start_time (Optional[datetime]): The optional inclusive start date to filter scenarios by creation date.
+        created_end_time (Optional[datetime]): The optional inclusive end date to filter scenarios by creation date.
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
@@ -548,6 +551,8 @@ def get_scenarios(
     else:
     else:
         scenarios = []
         scenarios = []
 
 
+    if created_start_time or created_end_time:
+        scenarios = scenario_manager._filter_by_creation_time(scenarios, created_start_time, created_end_time)
     if is_sorted:
     if is_sorted:
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
     return scenarios
     return scenarios
@@ -569,6 +574,8 @@ def get_primary(cycle: Cycle) -> Optional[Scenario]:
 def get_primary_scenarios(
 def get_primary_scenarios(
     is_sorted: bool = False,
     is_sorted: bool = False,
     descending: bool = False,
     descending: bool = False,
+    created_start_time: Optional[datetime] = None,
+    created_end_time: Optional[datetime] = None,
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
     sort_key: Literal["name", "id", "config_id", "creation_date", "tags"] = "name",
 ) -> List[Scenario]:
 ) -> List[Scenario]:
     """Retrieve a list of all primary scenarios.
     """Retrieve a list of all primary scenarios.
@@ -578,6 +585,8 @@ def get_primary_scenarios(
             The default value is False.
             The default value is False.
         descending (bool): If True, sort the output list of scenarios in descending order.
         descending (bool): If True, sort the output list of scenarios in descending order.
             The default value is False.
             The default value is False.
+        created_start_time (Optional[datetime]): The optional inclusive start date to filter scenarios by creation date.
+        created_end_time (Optional[datetime]): The optional inclusive end date to filter scenarios by creation date.
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
         sort_key (Literal["name", "id", "creation_date", "tags"]): The optional sort_key to
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             decide upon what key scenarios are sorted. The sorting is in increasing order for
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
             dates, in alphabetical order for name and id, and in lexicographical order for tags.
@@ -589,6 +598,9 @@ def get_primary_scenarios(
     """
     """
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenario_manager = _ScenarioManagerFactory._build_manager()
     scenarios = scenario_manager._get_primary_scenarios()
     scenarios = scenario_manager._get_primary_scenarios()
+
+    if created_start_time or created_end_time:
+        scenarios = scenario_manager._filter_by_creation_time(scenarios, created_start_time, created_end_time)
     if is_sorted:
     if is_sorted:
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
         scenario_manager._sort_scenarios(scenarios, descending, sort_key)
     return scenarios
     return scenarios
@@ -867,7 +879,7 @@ def get_cycles() -> List[Cycle]:
     return _CycleManagerFactory._build_manager()._get_all()
     return _CycleManagerFactory._build_manager()._get_all()
 
 
 
 
-def can_create(config: Optional[Union[ScenarioConfig, DataNodeConfig]] = None) -> Reasons:
+def can_create(config: Optional[Union[ScenarioConfig, DataNodeConfig]] = None) -> ReasonCollection:
     """Indicate if a config can be created. The config should be a scenario or data node config.
     """Indicate if a config can be created. The config should be a scenario or data node config.
 
 
     If no config is provided, the function indicates if any scenario or data node config can be created.
     If no config is provided, the function indicates if any scenario or data node config can be created.

+ 8 - 13
taipy/core/task/_task_manager.py

@@ -26,12 +26,7 @@ from ..cycle.cycle_id import CycleId
 from ..data._data_manager_factory import _DataManagerFactory
 from ..data._data_manager_factory import _DataManagerFactory
 from ..exceptions.exceptions import NonExistingTask
 from ..exceptions.exceptions import NonExistingTask
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import (
-    _build_data_node_is_being_edited_reason,
-    _build_data_node_is_not_written,
-    _build_not_submittable_entity_reason,
-)
-from ..reason.reason import Reasons
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten, EntityIsNotSubmittableEntity, ReasonCollection
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..sequence.sequence_id import SequenceId
 from ..submission.submission import Submission
 from ..submission.submission import Submission
@@ -169,24 +164,24 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
         return entity_ids
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reasons:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> ReasonCollection:
         if isinstance(task, str):
         if isinstance(task, str):
             task = cls._get(task)
             task = cls._get(task)
+
+        reason_collection = ReasonCollection()
         if not isinstance(task, Task):
         if not isinstance(task, Task):
             task = str(task)
             task = str(task)
-            reason = Reasons(task)
-            reason._add_reason(task, _build_not_submittable_entity_reason(task))
+            reason_collection._add_reason(task, EntityIsNotSubmittableEntity(task))
         else:
         else:
-            reason = Reasons(task.id)
             data_manager = _DataManagerFactory._build_manager()
             data_manager = _DataManagerFactory._build_manager()
             for node in task.input.values():
             for node in task.input.values():
                 node = data_manager._get(node)
                 node = data_manager._get(node)
                 if node._edit_in_progress:
                 if node._edit_in_progress:
-                    reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
+                    reason_collection._add_reason(node.id, DataNodeEditInProgress(node.id))
                 if not node._last_edit_date:
                 if not node._last_edit_date:
-                    reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
+                    reason_collection._add_reason(node.id, DataNodeIsNotWritten(node.id))
 
 
-        return reason
+        return reason_collection
 
 
     @classmethod
     @classmethod
     def _submit(
     def _submit(

+ 14 - 3
taipy/gui/_renderers/builder.py

@@ -159,7 +159,7 @@ class _Builder:
             if hashname is None:
             if hashname is None:
                 if callable(v):
                 if callable(v):
                     if v.__name__ == "<lambda>":
                     if v.__name__ == "<lambda>":
-                        hashname = _get_expr_var_name(v.__code__)
+                        hashname = f"__lambda_{id(v)}"
                         gui._bind_var_val(hashname, v)
                         gui._bind_var_val(hashname, v)
                     else:
                     else:
                         hashname = _get_expr_var_name(v.__name__)
                         hashname = _get_expr_var_name(v.__name__)
@@ -312,6 +312,15 @@ class _Builder:
             return self
             return self
         return self.set_attribute(_to_camel_case(name), str(strattr))
         return self.set_attribute(_to_camel_case(name), str(strattr))
 
 
+    def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None):
+        date_attr = self.__attributes.get(var_name, default_value)
+        if date_attr is None:
+            date_attr = default_value
+        if isinstance(date_attr, (datetime, date, time)):
+            value = _date_to_string(date_attr)
+            self.set_attribute(_to_camel_case(var_name), value)
+        return self
+
     def __set_dynamic_string_attribute(
     def __set_dynamic_string_attribute(
         self,
         self,
         name: str,
         name: str,
@@ -395,7 +404,7 @@ class _Builder:
                 adapter = self.__gui._get_adapter_for_type(var_type)
                 adapter = self.__gui._get_adapter_for_type(var_type)
             elif var_type == str.__name__ and callable(adapter):
             elif var_type == str.__name__ and callable(adapter):
                 var_type += (
                 var_type += (
-                    _get_expr_var_name(str(adapter.__code__))
+                    f"__lambda_{id(adapter)}"
                     if adapter.__name__ == "<lambda>"
                     if adapter.__name__ == "<lambda>"
                     else _get_expr_var_name(adapter.__name__)
                     else _get_expr_var_name(adapter.__name__)
                 )
                 )
@@ -879,7 +888,7 @@ class _Builder:
     def _set_input_type(self, type_name: str, allow_password=False):
     def _set_input_type(self, type_name: str, allow_password=False):
         if allow_password and self.__get_boolean_attribute("password", False):
         if allow_password and self.__get_boolean_attribute("password", False):
             return self.set_attribute("type", "password")
             return self.set_attribute("type", "password")
-        return self.set_attribute("type", type_name)
+        return self.set_attribute("type", self.__attributes.get("type", type_name))
 
 
     def _set_kind(self):
     def _set_kind(self):
         if self.__attributes.get("theme", False):
         if self.__attributes.get("theme", False):
@@ -1012,6 +1021,8 @@ class _Builder:
                     self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
                     self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
                 else:
                 else:
                     self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
                     self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dynamic_date:
+                self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.data:
             elif var_type == PropertyType.data:
                 self.__set_dynamic_property_without_default(attr[0], var_type)
                 self.__set_dynamic_property_without_default(attr[0], var_type)
             elif var_type == PropertyType.lov or var_type == PropertyType.single_lov:
             elif var_type == PropertyType.lov or var_type == PropertyType.single_lov:

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

@@ -138,6 +138,8 @@ class _Factory:
             [
             [
                 ("with_time", PropertyType.boolean),
                 ("with_time", PropertyType.boolean),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("active", PropertyType.dynamic_boolean, True),
+                ("min", PropertyType.dynamic_date),
+                ("max", PropertyType.dynamic_date),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("hover_text", PropertyType.dynamic_string),
                 ("label",),
                 ("label",),
@@ -386,6 +388,10 @@ class _Factory:
         .set_attributes(
         .set_attributes(
             [
             [
                 ("active", PropertyType.dynamic_boolean, True),
                 ("active", PropertyType.dynamic_boolean, True),
+                ("step", PropertyType.dynamic_number, 1),
+                ("step_multiplier", PropertyType.dynamic_number, 10),
+                ("min", PropertyType.dynamic_number),
+                ("max", PropertyType.dynamic_number),
                 ("hover_text", PropertyType.dynamic_string),
                 ("hover_text", PropertyType.dynamic_string),
                 ("on_change", PropertyType.function),
                 ("on_change", PropertyType.function),
                 ("on_action", PropertyType.function),
                 ("on_action", PropertyType.function),

+ 0 - 1
taipy/gui/builder/_api_generator.py

@@ -120,7 +120,6 @@ class _ElementApiGenerator(object, metaclass=_Singleton):
             classname,
             classname,
             (ElementBaseClass,),
             (ElementBaseClass,),
             {
             {
-                "__init__": lambda self, *args, **kwargs: ElementBaseClass.__init__(self, *args, **kwargs),
                 "_ELEMENT_NAME": element_name,
                 "_ELEMENT_NAME": element_name,
                 "_DEFAULT_PROPERTY": default_property,
                 "_DEFAULT_PROPERTY": default_property,
             },
             },

+ 69 - 6
taipy/gui/builder/_element.py

@@ -11,14 +11,26 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import ast
 import copy
 import copy
+import inspect
+import io
 import re
 import re
+import sys
 import typing as t
 import typing as t
+import uuid
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from collections.abc import Iterable
 from collections.abc import Iterable
+from types import FrameType, FunctionType
 
 
+from .._warnings import _warn
+from ..utils import _getscopeattr
+
+if sys.version_info < (3, 9):
+    from ..utils.unparse import _Unparser
 from ._context_manager import _BuilderContextManager
 from ._context_manager import _BuilderContextManager
 from ._factory import _BuilderFactory
 from ._factory import _BuilderFactory
+from ._utils import _LambdaByName, _python_builtins, _TransformVarToValue
 
 
 if t.TYPE_CHECKING:
 if t.TYPE_CHECKING:
     from ..gui import Gui
     from ..gui import Gui
@@ -30,6 +42,7 @@ class _Element(ABC):
     _ELEMENT_NAME = ""
     _ELEMENT_NAME = ""
     _DEFAULT_PROPERTY = ""
     _DEFAULT_PROPERTY = ""
     __RE_INDEXED_PROPERTY = re.compile(r"^(.*?)__([\w\d]+)$")
     __RE_INDEXED_PROPERTY = re.compile(r"^(.*?)__([\w\d]+)$")
+    _NEW_LAMBDA_NAME = "new_lambda"
 
 
     def __new__(cls, *args, **kwargs):
     def __new__(cls, *args, **kwargs):
         obj = super(_Element, cls).__new__(cls)
         obj = super(_Element, cls).__new__(cls)
@@ -40,6 +53,11 @@ class _Element(ABC):
 
 
     def __init__(self, *args, **kwargs) -> None:
     def __init__(self, *args, **kwargs) -> None:
         self._properties: t.Dict[str, t.Any] = {}
         self._properties: t.Dict[str, t.Any] = {}
+        self._lambdas: t.Dict[str, str] = {}
+        self.__calling_frame = t.cast(
+            FrameType, t.cast(FrameType, t.cast(FrameType, inspect.currentframe()).f_back).f_back
+        )
+
         if args and self._DEFAULT_PROPERTY != "":
         if args and self._DEFAULT_PROPERTY != "":
             self._properties = {self._DEFAULT_PROPERTY: args[0]}
             self._properties = {self._DEFAULT_PROPERTY: args[0]}
         self._properties.update(kwargs)
         self._properties.update(kwargs)
@@ -49,10 +67,15 @@ class _Element(ABC):
         self._properties.update(kwargs)
         self._properties.update(kwargs)
         self.parse_properties()
         self.parse_properties()
 
 
-    # Convert property value to string
+    def _evaluate_lambdas(self, gui: Gui):
+        for k, lmbd in self._lambdas.items():
+            expr = gui._evaluate_expr(f"{{{lmbd}}}")
+            gui._bind_var_val(k, _getscopeattr(gui, expr))
+
+    # Convert property value to string/function
     def parse_properties(self):
     def parse_properties(self):
         self._properties = {
         self._properties = {
-            _Element._parse_property_key(k): _Element._parse_property(v) for k, v in self._properties.items()
+            _Element._parse_property_key(k): self._parse_property(k, v) for k, v in self._properties.items()
         }
         }
 
 
     # Get a deepcopy version of the properties
     # Get a deepcopy version of the properties
@@ -65,10 +88,47 @@ class _Element(ABC):
             return f"{match.group(1)}[{match.group(2)}]"
             return f"{match.group(1)}[{match.group(2)}]"
         return key
         return key
 
 
-    @staticmethod
-    def _parse_property(value: t.Any) -> t.Any:
+    def _parse_property(self, key: str, value: t.Any) -> t.Any:
         if isinstance(value, (str, dict, Iterable)):
         if isinstance(value, (str, dict, Iterable)):
             return value
             return value
+        if isinstance(value, FunctionType):
+            if key.startswith("on_"):
+                if value.__name__.startswith("<"):
+                    return value
+                return value.__name__
+
+            try:
+                st = ast.parse(inspect.getsource(value.__code__).strip())
+                lambda_by_name: t.Dict[str, ast.Lambda] = {}
+                _LambdaByName(self._ELEMENT_NAME, lambda_by_name).visit(st)
+                lambda_fn = lambda_by_name.get(
+                    key,
+                    lambda_by_name.get(_LambdaByName._DEFAULT_NAME, None) if key == self._DEFAULT_PROPERTY else None,
+                )
+                if lambda_fn is not None:
+                    args = [arg.arg for arg in lambda_fn.args.args]
+                    targets = [
+                        compr.target.id  # type: ignore[attr-defined]
+                        for node in ast.walk(lambda_fn.body)
+                        if isinstance(node, ast.ListComp)
+                        for compr in node.generators
+                    ]
+                    tree = _TransformVarToValue(self.__calling_frame, args + targets + _python_builtins).visit(
+                        lambda_fn
+                    )
+                    ast.fix_missing_locations(tree)
+                    if sys.version_info < (3, 9):  # python 3.8 ast has no unparse
+                        string_fd = io.StringIO()
+                        _Unparser(tree, string_fd)
+                        string_fd.seek(0)
+                        lambda_text = string_fd.read()
+                    else:
+                        lambda_text = ast.unparse(tree)
+                    lambda_name = f"__lambda_{uuid.uuid4().hex}"
+                    self._lambdas[lambda_name] = lambda_text
+                    return f'{{{lambda_name}({", ".join(args)})}}'
+            except Exception as e:
+                _warn("Error in lambda expression", e)
         if hasattr(value, "__name__"):
         if hasattr(value, "__name__"):
             return str(getattr(value, "__name__"))  # noqa: B009
             return str(getattr(value, "__name__"))  # noqa: B009
         return str(value)
         return str(value)
@@ -99,6 +159,7 @@ class _Block(_Element):
         _BuilderContextManager().pop()
         _BuilderContextManager().pop()
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
+        self._evaluate_lambdas(gui)
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         return f"{el[0]}{self._render_children(gui)}</{el[1]}>"
         return f"{el[0]}{self._render_children(gui)}</{el[1]}>"
 
 
@@ -109,7 +170,7 @@ class _Block(_Element):
 class _DefaultBlock(_Block):
 class _DefaultBlock(_Block):
     _ELEMENT_NAME = "part"
     _ELEMENT_NAME = "part"
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, **kwargs):  # do not remove as it could break the search in frames
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
 
 
@@ -162,6 +223,7 @@ class html(_Block):
         self._content = args[1] if len(args) > 1 else ""
         self._content = args[1] if len(args) > 1 else ""
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
+        self._evaluate_lambdas(gui)
         if self._ELEMENT_NAME:
         if self._ELEMENT_NAME:
             attrs = ""
             attrs = ""
             if self._properties:
             if self._properties:
@@ -174,10 +236,11 @@ class html(_Block):
 class _Control(_Element):
 class _Control(_Element):
     """NOT DOCUMENTED"""
     """NOT DOCUMENTED"""
 
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, **kwargs):  # do not remove as it could break the search in frames
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
     def _render(self, gui: "Gui") -> str:
     def _render(self, gui: "Gui") -> str:
+        self._evaluate_lambdas(gui)
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         el = _BuilderFactory.create_element(gui, self._ELEMENT_NAME, self._deepcopy_properties())
         return (
         return (
             f"<div>{el[0]}</{el[1]}></div>"
             f"<div>{el[0]}</{el[1]}></div>"

+ 65 - 0
taipy/gui/builder/_utils.py

@@ -0,0 +1,65 @@
+# 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.
+
+from __future__ import annotations
+
+import ast
+import builtins
+import typing as t
+from operator import attrgetter
+from types import FrameType
+
+_python_builtins = dir(builtins)
+
+
+def _get_value_in_frame(frame: FrameType, name: str):
+    if not frame:
+        return None
+    if name in frame.f_locals:
+        return frame.f_locals.get(name)
+    return _get_value_in_frame(t.cast(FrameType, frame.f_back), name)
+
+
+class _TransformVarToValue(ast.NodeTransformer):
+    def __init__(self, frame: FrameType, non_vars: t.List[str]) -> None:
+        super().__init__()
+        self.frame = frame
+        self.non_vars = non_vars
+
+    def visit_Name(self, node):
+        var_parts = node.id.split(".", 2)
+        if var_parts[0] in self.non_vars:
+            return node
+        value = _get_value_in_frame(self.frame, var_parts[0])
+        if callable(value):
+            return node
+        if len(var_parts) > 1:
+            value = attrgetter(var_parts[1])(value)
+        return ast.Constant(value=value, kind=None)
+
+
+class _LambdaByName(ast.NodeVisitor):
+    _DEFAULT_NAME = "<default>"
+
+    def __init__(self, element_name: str, lambdas: t.Dict[str, ast.Lambda]) -> None:
+        super().__init__()
+        self.element_name = element_name
+        self.lambdas = lambdas
+
+    def visit_Call(self, node):
+        if node.func.attr == self.element_name:
+            if self.lambdas.get(_LambdaByName._DEFAULT_NAME, None) is None:
+                self.lambdas[_LambdaByName._DEFAULT_NAME] = next(
+                    (arg for arg in node.args if isinstance(arg, ast.Lambda)), None
+                )
+            for kwd in node.keywords:
+                if isinstance(kwd.value, ast.Lambda):
+                    self.lambdas[kwd.arg] = kwd.value

+ 129 - 45
taipy/gui/gui.py

@@ -49,7 +49,7 @@ import __main__  # noqa: F401
 from taipy.logger._taipy_logger import _TaipyLogger
 from taipy.logger._taipy_logger import _TaipyLogger
 
 
 if util.find_spec("pyngrok"):
 if util.find_spec("pyngrok"):
-    from pyngrok import ngrok
+    from pyngrok import ngrok  # type: ignore[reportMissingImports]
 
 
 from ._default_config import _default_stylekit, default_config
 from ._default_config import _default_stylekit, default_config
 from ._page import _Page
 from ._page import _Page
@@ -320,7 +320,7 @@ class Gui:
         self.__locals_context = _LocalsContext()
         self.__locals_context = _LocalsContext()
         self.__var_dir = _VariableDirectory(self.__locals_context)
         self.__var_dir = _VariableDirectory(self.__locals_context)
 
 
-        self.__evaluator: _Evaluator = None  # type: ignore
+        self.__evaluator: _Evaluator = None  # type: ignore[assignment]
         self.__adapter = _Adapter()
         self.__adapter = _Adapter()
         self.__directory_name_of_pages: t.List[str] = []
         self.__directory_name_of_pages: t.List[str] = []
         self.__favicon: t.Optional[t.Union[str, Path]] = None
         self.__favicon: t.Optional[t.Union[str, Path]] = None
@@ -437,7 +437,7 @@ class Gui:
             if provider_fn is None:
             if provider_fn is None:
                 # try plotly
                 # try plotly
                 if find_spec("plotly") and find_spec("plotly.graph_objs"):
                 if find_spec("plotly") and find_spec("plotly.graph_objs"):
-                    from plotly.graph_objs import Figure as PlotlyFigure
+                    from plotly.graph_objs import Figure as PlotlyFigure  # type: ignore[reportMissingImports]
 
 
                     if isinstance(content, PlotlyFigure):
                     if isinstance(content, PlotlyFigure):
 
 
@@ -509,6 +509,10 @@ class Gui:
     def _get_shared_variables(self) -> t.List[str]:
     def _get_shared_variables(self) -> t.List[str]:
         return self.__evaluator.get_shared_variables()
         return self.__evaluator.get_shared_variables()
 
 
+    @staticmethod
+    def _clear_shared_variable() -> None:
+        Gui.__shared_variables.clear()
+
     def __get_content_accessor(self):
     def __get_content_accessor(self):
         if self.__content_accessor is None:
         if self.__content_accessor is None:
             self.__content_accessor = _ContentAccessor(self._get_config("data_url_max_size", 50 * 1024))
             self.__content_accessor = _ContentAccessor(self._get_config("data_url_max_size", 50 * 1024))
@@ -1129,7 +1133,8 @@ class Gui:
                     {
                     {
                         "type": _WsType.GET_MODULE_CONTEXT.value,
                         "type": _WsType.GET_MODULE_CONTEXT.value,
                         "payload": {"context": mc, "metadata": meta_return},
                         "payload": {"context": mc, "metadata": meta_return},
-                    }
+                    },
+                    send_back_only=True,
                 )
                 )
 
 
     def __get_variable_tree(self, data: t.Dict[str, t.Any]):
     def __get_variable_tree(self, data: t.Dict[str, t.Any]):
@@ -1173,7 +1178,8 @@ class Gui:
                     "variable": self.__get_variable_tree(data),
                     "variable": self.__get_variable_tree(data),
                     "function": self.__get_variable_tree(function_data),
                     "function": self.__get_variable_tree(function_data),
                 },
                 },
-            }
+            },
+            send_back_only=True,
         )
         )
 
 
     def __handle_ws_app_id(self, message: t.Any):
     def __handle_ws_app_id(self, message: t.Any):
@@ -1188,7 +1194,8 @@ class Gui:
             {
             {
                 "type": _WsType.APP_ID.value,
                 "type": _WsType.APP_ID.value,
                 "payload": {"name": name, "id": app_id},
                 "payload": {"name": name, "id": app_id},
-            }
+            },
+            send_back_only=True,
         )
         )
 
 
     def __handle_ws_get_routes(self):
     def __handle_ws_get_routes(self):
@@ -1206,7 +1213,8 @@ class Gui:
             {
             {
                 "type": _WsType.GET_ROUTES.value,
                 "type": _WsType.GET_ROUTES.value,
                 "payload": routes,
                 "payload": routes,
-            }
+            },
+            send_back_only=True,
         )
         )
 
 
     def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None:
     def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None:
@@ -1406,7 +1414,7 @@ class Gui:
             try:
             try:
                 fd, temp_path = mkstemp(".csv", var_name, text=True)
                 fd, temp_path = mkstemp(".csv", var_name, text=True)
                 with os.fdopen(fd, "wt", newline="") as csv_file:
                 with os.fdopen(fd, "wt", newline="") as csv_file:
-                    df.to_csv(csv_file, index=False)  # type:ignore
+                    df.to_csv(csv_file, index=False)  # type: ignore[union-attr]
                 self._download(temp_path, "data.csv", Gui.__DOWNLOAD_DELETE_ACTION)
                 self._download(temp_path, "data.csv", Gui.__DOWNLOAD_DELETE_ACTION)
             except Exception as e:  # pragma: no cover
             except Exception as e:  # pragma: no cover
                 if not self._call_on_exception("download_csv", e):
                 if not self._call_on_exception("download_csv", e):
@@ -1467,60 +1475,135 @@ class Gui:
                     _warn(f"on_action(): Exception raised in '{action_function.__name__}()'", e)
                     _warn(f"on_action(): Exception raised in '{action_function.__name__}()'", e)
         return False
         return False
 
 
-    def _call_function_with_state(self, user_function: t.Callable, args: t.List[t.Any]) -> t.Any:
-        args.insert(0, self.__get_state())
+    def _call_function_with_state(self, user_function: t.Callable, args: t.Optional[t.List[t.Any]] = None) -> t.Any:
+        cp_args = [] if args is None else args.copy()
+        cp_args.insert(0, self.__get_state())
         argcount = user_function.__code__.co_argcount
         argcount = user_function.__code__.co_argcount
         if argcount > 0 and inspect.ismethod(user_function):
         if argcount > 0 and inspect.ismethod(user_function):
             argcount -= 1
             argcount -= 1
-        if argcount > len(args):
-            args += (argcount - len(args)) * [None]
+        if argcount > len(cp_args):
+            cp_args += (argcount - len(cp_args)) * [None]
         else:
         else:
-            args = args[:argcount]
-        return user_function(*args)
+            cp_args = cp_args[:argcount]
+        return user_function(*cp_args)
 
 
     def _set_module_context(self, module_context: t.Optional[str]) -> t.ContextManager[None]:
     def _set_module_context(self, module_context: t.Optional[str]) -> t.ContextManager[None]:
         return self._set_locals_context(module_context) if module_context is not None else contextlib.nullcontext()
         return self._set_locals_context(module_context) if module_context is not None else contextlib.nullcontext()
 
 
-    def _call_user_callback(
+    def invoke_callback(
         self,
         self,
-        state_id: t.Optional[str],
-        user_callback: t.Union[t.Callable, str],
-        args: t.List[t.Any],
-        module_context: t.Optional[str],
+        state_id: str,
+        callback: t.Callable,
+        args: t.Optional[t.Sequence[t.Any]] = None,
+        module_context: t.Optional[str] = None,
     ) -> t.Any:
     ) -> t.Any:
+        """Invoke a user callback for a given state.
+
+        See the
+        [section on Long Running Callbacks in a Thread](../gui/callbacks.md#long-running-callbacks-in-a-thread)
+        in the User Manual for details on when and how this function can be used.
+
+        Arguments:
+            state_id: The identifier of the state to use, as returned by `get_state_id()^`.
+            callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
+                The first parameter of this function **must** be a `State^`.
+            args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
+            module_context (Optional[str]): the name of the module that will be used.
+        """
+        this_sid = None
+        if request:
+            # avoid messing with the client_id => Set(ws id)
+            this_sid = getattr(request, "sid", None)
+            request.sid = None  # type: ignore[attr-defined]
         try:
         try:
             with self.get_flask_app().app_context():
             with self.get_flask_app().app_context():
-                self.__set_client_id_in_context(state_id)
+                setattr(g, Gui.__ARG_CLIENT_ID, state_id)
                 with self._set_module_context(module_context):
                 with self._set_module_context(module_context):
-                    if not callable(user_callback):
-                        user_callback = self._get_user_function(user_callback)
-                    if not callable(user_callback):
-                        _warn(f"invoke_callback(): {user_callback} is not callable.")
+                    if not callable(callback):
+                        callback = self._get_user_function(callback)
+                    if not callable(callback):
+                        _warn(f"invoke_callback(): {callback} is not callable.")
                         return None
                         return None
-                    return self._call_function_with_state(user_callback, args)
+                    return self._call_function_with_state(callback, list(args) if args else None)
         except Exception as e:  # pragma: no cover
         except Exception as e:  # pragma: no cover
-            if not self._call_on_exception(user_callback.__name__ if callable(user_callback) else user_callback, e):
+            if not self._call_on_exception(callback.__name__ if callable(callback) else callback, e):
                 _warn(
                 _warn(
-                    "invoke_callback(): Exception raised in "
-                    + f"'{user_callback.__name__ if callable(user_callback) else user_callback}()'",
+                    "Gui.invoke_callback(): Exception raised in "
+                    + f"'{callback.__name__ if callable(callback) else callback}()'",
                     e,
                     e,
                 )
                 )
+        finally:
+            if this_sid and request:
+                request.sid = this_sid  # type: ignore[attr-defined]
         return None
         return None
 
 
-    def _call_broadcast_callback(
-        self, user_callback: t.Callable, args: t.List[t.Any], module_context: t.Optional[str]
-    ) -> t.Any:
-        @contextlib.contextmanager
-        def _broadcast_callback() -> t.Iterator[None]:
-            try:
-                setattr(g, Gui.__BRDCST_CALLBACK_G_ID, True)
-                yield
-            finally:
-                setattr(g, Gui.__BRDCST_CALLBACK_G_ID, False)
+    def broadcast_callback(
+        self,
+        callback: t.Callable,
+        args: t.Optional[t.Sequence[t.Any]] = None,
+        module_context: t.Optional[str] = None,
+    ) -> t.Dict[str, t.Any]:
+        """Invoke a callback for every client.
 
 
-        with _broadcast_callback():
-            # Use global scopes for broadcast callbacks
-            return self._call_user_callback(_DataScopes._GLOBAL_ID, user_callback, args, module_context)
+        This callback gets invoked for every client connected to the application with the appropriate
+        `State^` instance. You can then perform client-specific tasks, such as updating the state
+        variable reflected in the user interface.
+
+        Arguments:
+            gui (Gui^): The current Gui instance.
+            callback: The user-defined function to be invoked.<br/>
+                The first parameter of this function must be a `State^` object representing the
+                client for which it is invoked.<br/>
+                The other parameters should reflect the ones provided in the *args* collection.
+            args: The parameters to send to *callback*, if any.
+        """
+        # Iterate over all the scopes
+        res = {}
+        for id in [id for id in self.__bindings._get_all_scopes() if id != _DataScopes._GLOBAL_ID]:
+            ret = self.invoke_callback(id, callback, args, module_context)
+            res[id] = ret
+        return res
+
+    def broadcast_change(self, var_name: str, value: t.Any):
+        """Propagates a new value for a given variable to all states.
+
+        This callback gets invoked for every client connected to the application to update the value
+        of the variable called *var_name* to the new value *value*, in their specific `State^`
+        instance. All user interfaces reflect the change.
+
+        Arguments:
+            gui (Gui^): The current Gui instance.
+            var_name: The name of the variable to change.
+            value: The new value for the variable.
+        """
+        self.broadcast_callback(lambda s, n, v: s.assign(n, v), [var_name, value])
+
+    @staticmethod
+    def __broadcast_changes_fn(state: State, values: dict[str, t.Any]) -> None:
+        with state:
+            for n, v in values.items():
+                state.assign(n, v)
+
+    def broadcast_changes(self, values: t.Optional[dict[str, t.Any]] = None, **kwargs):
+        """Propagates new values for several variables to all states.
+
+        This callback gets invoked for every client connected to the application to update the value
+        of all the variables that are keys in *values*, in their specific `State^` instance. All
+        user interfaces reflect the change.
+
+        Arguments:
+            values: An optional dictionary where each key is the name of a variable to change, and
+                where the associated value is the new value to set for that variable, in each state
+                for the application.
+            **kwargs: A collection of variable name-value pairs that are updated for each state of
+                the application. Name-value pairs overload the ones in *values* if the name exists
+                as a key in the dictionary.
+        """
+        if kwargs:
+            values = values.copy() if values else {}
+            for n, v in kwargs.items():
+                values[n] = v
+        self.broadcast_callback(Gui.__broadcast_changes_fn, [values])
 
 
     def _is_in_brdcst_callback(self):
     def _is_in_brdcst_callback(self):
         try:
         try:
@@ -2094,7 +2177,7 @@ class Gui:
                 if not isinstance(lib, ElementLibrary):
                 if not isinstance(lib, ElementLibrary):
                     continue
                     continue
                 try:
                 try:
-                    self._call_function_with_state(lib.on_user_init, [])
+                    self._call_function_with_state(lib.on_user_init)
                 except Exception as e:  # pragma: no cover
                 except Exception as e:  # pragma: no cover
                     if not self._call_on_exception(f"{name}.on_user_init", e):
                     if not self._call_on_exception(f"{name}.on_user_init", e):
                         _warn(f"Exception raised in {name}.on_user_init()", e)
                         _warn(f"Exception raised in {name}.on_user_init()", e)
@@ -2107,7 +2190,7 @@ class Gui:
             self.__init_libs()
             self.__init_libs()
             if hasattr(self, "on_init") and callable(self.on_init):
             if hasattr(self, "on_init") and callable(self.on_init):
                 try:
                 try:
-                    self._call_function_with_state(self.on_init, [])
+                    self._call_function_with_state(self.on_init)
                 except Exception as e:  # pragma: no cover
                 except Exception as e:  # pragma: no cover
                     if not self._call_on_exception("on_init", e):
                     if not self._call_on_exception("on_init", e):
                         _warn("Exception raised in on_init()", e)
                         _warn("Exception raised in on_init()", e)
@@ -2298,7 +2381,7 @@ class Gui:
             config["extensions"] = {}
             config["extensions"] = {}
             for libs in self.__extensions.values():
             for libs in self.__extensions.values():
                 for lib in libs:
                 for lib in libs:
-                    config["extensions"][f"./{Gui._EXTENSION_ROOT}/{lib.get_js_module_name()}"] = [  # type: ignore
+                    config["extensions"][f"./{Gui._EXTENSION_ROOT}/{lib.get_js_module_name()}"] = [
                         e._get_js_name(n)
                         e._get_js_name(n)
                         for n, e in lib.get_elements().items()
                         for n, e in lib.get_elements().items()
                         if isinstance(e, Element) and not e._is_server_only()
                         if isinstance(e, Element) and not e._is_server_only()
@@ -2553,8 +2636,9 @@ class Gui:
 
 
         # Base global ctx is TaipyHolder classes + script modules and callables
         # Base global ctx is TaipyHolder classes + script modules and callables
         glob_ctx: t.Dict[str, t.Any] = {t.__name__: t for t in _TaipyBase.__subclasses__()}
         glob_ctx: t.Dict[str, t.Any] = {t.__name__: t for t in _TaipyBase.__subclasses__()}
-        glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
         glob_ctx[Gui.__SELF_VAR] = self
         glob_ctx[Gui.__SELF_VAR] = self
+        glob_ctx["state"] = self.__state
+        glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
 
 
         # Call on_init on each library
         # Call on_init on each library
         for name, libs in self.__extensions.items():
         for name, libs in self.__extensions.items():

+ 14 - 18
taipy/gui/gui_actions.py

@@ -267,39 +267,37 @@ def invoke_callback(
     gui: Gui,
     gui: Gui,
     state_id: str,
     state_id: str,
     callback: t.Callable,
     callback: t.Callable,
-    args: t.Union[t.Tuple, t.List],
+    args: t.Optional[t.Sequence[t.Any]] = None,
     module_context: t.Optional[str] = None,
     module_context: t.Optional[str] = None,
 ) -> t.Any:
 ) -> t.Any:
     """Invoke a user callback for a given state.
     """Invoke a user callback for a given state.
 
 
-    See the
-    [User Manual section on Long Running Callbacks in a Thread](../gui/callbacks.md#long-running-callbacks-in-a-thread)
-    for details on when and how this function can be used.
+    Calling this function is equivalent to calling
+    *gui*.[Gui.]invoke_callback(state_id, callback, args, module_context)^`.
 
 
     Arguments:
     Arguments:
         gui (Gui^): The current Gui instance.
         gui (Gui^): The current Gui instance.
         state_id: The identifier of the state to use, as returned by `get_state_id()^`.
         state_id: The identifier of the state to use, as returned by `get_state_id()^`.
         callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
         callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
             The first parameter of this function **must** be a `State^`.
             The first parameter of this function **must** be a `State^`.
-        args (Union[Tuple, List]): The remaining arguments, as a List or a Tuple.
+        args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
         module_context (Optional[str]): the name of the module that will be used.
         module_context (Optional[str]): the name of the module that will be used.
     """
     """
     if isinstance(gui, Gui):
     if isinstance(gui, Gui):
-        return gui._call_user_callback(state_id, callback, list(args), module_context)
+        return gui.invoke_callback(state_id, callback, args, module_context)
     _warn("'invoke_callback()' must be called with a valid Gui instance.")
     _warn("'invoke_callback()' must be called with a valid Gui instance.")
 
 
 
 
 def broadcast_callback(
 def broadcast_callback(
     gui: Gui,
     gui: Gui,
     callback: t.Callable,
     callback: t.Callable,
-    args: t.Optional[t.Union[t.Tuple, t.List]] = None,
+    args: t.Optional[t.Sequence[t.Any]] = None,
     module_context: t.Optional[str] = None,
     module_context: t.Optional[str] = None,
-) -> t.Any:
+) -> t.Dict[str, t.Any]:
     """Invoke a callback for every client.
     """Invoke a callback for every client.
 
 
-    This callback gets invoked for every client connected to the application with the appropriate
-    `State^` instance. You can then perform client-specific tasks, such as updating the state
-    variable reflected in the user interface.
+    Calling this function is equivalent to calling
+    *gui*.[Gui.]broadcast_callback(callback, args, module_context)^`.
 
 
     Arguments:
     Arguments:
         gui (Gui^): The current Gui instance.
         gui (Gui^): The current Gui instance.
@@ -310,13 +308,13 @@ def broadcast_callback(
         args: The parameters to send to *callback*, if any.
         args: The parameters to send to *callback*, if any.
     """
     """
     if isinstance(gui, Gui):
     if isinstance(gui, Gui):
-        return gui._call_broadcast_callback(callback, list(args) if args else [], module_context)
+        return gui.broadcast_callback(callback, args, module_context)
     _warn("'broadcast_callback()' must be called with a valid Gui instance.")
     _warn("'broadcast_callback()' must be called with a valid Gui instance.")
 
 
 
 
 def invoke_state_callback(gui: Gui, state_id: str, callback: t.Callable, args: t.Union[t.Tuple, t.List]) -> t.Any:
 def invoke_state_callback(gui: Gui, state_id: str, callback: t.Callable, args: t.Union[t.Tuple, t.List]) -> t.Any:
-    _warn("'invoke_state_callback()' was deprecated in Taipy GUI 2.0. Use 'invoke_callback()' instead.")
-    return invoke_callback(gui, state_id, callback, args)
+    _warn("'invoke_state_callback()' was deprecated in Taipy GUI 2.0. Use 'Gui.invoke_callback()' instead.")
+    return gui.invoke_callback(state_id, callback, args)
 
 
 
 
 def invoke_long_callback(
 def invoke_long_callback(
@@ -393,16 +391,14 @@ def invoke_long_callback(
         function_result: t.Optional[t.Any] = None,
         function_result: t.Optional[t.Any] = None,
     ):
     ):
         if callable(user_status_function):
         if callable(user_status_function):
-            invoke_callback(
-                this_gui,
+            this_gui.invoke_callback(
                 str(state_id),
                 str(state_id),
                 user_status_function,
                 user_status_function,
                 [status] + list(user_status_function_args) + [function_result],  # type: ignore
                 [status] + list(user_status_function_args) + [function_result],  # type: ignore
                 str(module_context),
                 str(module_context),
             )
             )
         if e:
         if e:
-            invoke_callback(
-                this_gui,
+            this_gui.invoke_callback(
                 str(state_id),
                 str(state_id),
                 callback_on_exception,
                 callback_on_exception,
                 (
                 (

+ 1 - 1
taipy/gui/server.py

@@ -169,7 +169,7 @@ class _Server:
                     return render_template(
                     return render_template(
                         "index.html",
                         "index.html",
                         title=title,
                         title=title,
-                        favicon=favicon,
+                        favicon=f"{favicon}?version={version}",
                         root_margin=root_margin,
                         root_margin=root_margin,
                         watermark=watermark,
                         watermark=watermark,
                         config=client_config,
                         config=client_config,

+ 12 - 5
taipy/gui/types.py

@@ -82,26 +82,33 @@ class PropertyType(Enum):
     """
     """
     The property holds a dictionary.
     The property holds a dictionary.
     """
     """
+    dynamic_date = "dynamicdate"
+    """
+    The property is dynamic and holds a date.
+    """
     dynamic_dict = _TaipyDict
     dynamic_dict = _TaipyDict
     """
     """
-    The property holds a dynamic dictionary.
+    The property is dynamic and holds a dictionary.
     """
     """
     dynamic_number = _TaipyNumber
     dynamic_number = _TaipyNumber
     """
     """
-    The property holds a dynamic number.
+    The property is dynamic and holds a number.
     """
     """
     dynamic_lo_numbers = _TaipyLoNumbers
     dynamic_lo_numbers = _TaipyLoNumbers
     """
     """
-    The property holds a dynamic list of numbers.
+    The property is dynamic and holds a list of numbers.
     """
     """
     dynamic_boolean = _TaipyBool
     dynamic_boolean = _TaipyBool
     """
     """
-    The property holds a dynamic Boolean value.
+    The property is dynamic and holds a Boolean value.
     """
     """
     dynamic_list = "dynamiclist"
     dynamic_list = "dynamiclist"
+    """
+    The property is dynamic and holds a list.
+    """
     dynamic_string = "dynamicstring"
     dynamic_string = "dynamicstring"
     """
     """
-    The property holds a dynamic string.
+    The property is dynamic and holds a string.
     """
     """
     function = "function"
     function = "function"
     """
     """

+ 731 - 0
taipy/gui/utils/unparse.py

@@ -0,0 +1,731 @@
+# 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.
+
+# from https://github.com/python/cpython/blob/3.8/Tools/parser/unparse.py to unparse an ast tree with python < 3.9
+
+import ast
+import io
+import sys
+
+# Large float and imaginary literals get turned into infinities in the AST.
+# We unparse those infinities to INFSTR.
+_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1)
+
+
+def _interleave(inter, f, seq):
+    """Call f on each item in seq, calling inter() in between."""
+    seq = iter(seq)
+    try:
+        f(next(seq))
+    except StopIteration:
+        pass
+    else:
+        for x in seq:
+            inter()
+            f(x)
+
+
+class _Unparser:
+    """Methods in this class recursively traverse an AST and
+    output source code for the abstract syntax; original formatting
+    is disregarded."""
+
+    def __init__(self, tree, file=sys.stdout):
+        """Unparser(tree, file=sys.stdout) -> None.
+        Print the source for tree to file."""
+        self.f = file
+        self._indent = 0
+        self.dispatch(tree)
+        print("", file=self.f)
+        self.f.flush()
+
+    def fill(self, text=""):
+        "Indent a piece of text, according to the current indentation level"
+        self.f.write("\n" + "    " * self._indent + text)
+
+    def write(self, text):
+        "Append a piece of text to the current line."
+        self.f.write(text)
+
+    def enter(self):
+        "Print ':', and increase the indentation."
+        self.write(":")
+        self._indent += 1
+
+    def leave(self):
+        "Decrease the indentation level."
+        self._indent -= 1
+
+    def dispatch(self, tree):
+        "Dispatcher function, dispatching tree type T to method _T."
+        if isinstance(tree, list):
+            for t in tree:
+                self.dispatch(t)
+            return
+        meth = getattr(self, "_" + tree.__class__.__name__)
+        meth(tree)
+
+    ############### Unparsing methods ######################
+    # There should be one method per concrete grammar type #
+    # Constructors should be grouped by sum type. Ideally, #
+    # this would follow the order in the grammar, but      #
+    # currently doesn't.                                   #
+    ########################################################
+
+    def _Module(self, tree):
+        for stmt in tree.body:
+            self.dispatch(stmt)
+
+    # stmt
+    def _Expr(self, tree):
+        self.fill()
+        self.dispatch(tree.value)
+
+    def _NamedExpr(self, tree):
+        self.write("(")
+        self.dispatch(tree.target)
+        self.write(" := ")
+        self.dispatch(tree.value)
+        self.write(")")
+
+    def _Import(self, t):
+        self.fill("import ")
+        _interleave(lambda: self.write(", "), self.dispatch, t.names)
+
+    def _ImportFrom(self, t):
+        self.fill("from ")
+        self.write("." * t.level)
+        if t.module:
+            self.write(t.module)
+        self.write(" import ")
+        _interleave(lambda: self.write(", "), self.dispatch, t.names)
+
+    def _Assign(self, t):
+        self.fill()
+        for target in t.targets:
+            self.dispatch(target)
+            self.write(" = ")
+        self.dispatch(t.value)
+
+    def _AugAssign(self, t):
+        self.fill()
+        self.dispatch(t.target)
+        self.write(" " + self.binop[t.op.__class__.__name__] + "= ")
+        self.dispatch(t.value)
+
+    def _AnnAssign(self, t):
+        self.fill()
+        if not t.simple and isinstance(t.target, ast.Name):
+            self.write("(")
+        self.dispatch(t.target)
+        if not t.simple and isinstance(t.target, ast.Name):
+            self.write(")")
+        self.write(": ")
+        self.dispatch(t.annotation)
+        if t.value:
+            self.write(" = ")
+            self.dispatch(t.value)
+
+    def _Return(self, t):
+        self.fill("return")
+        if t.value:
+            self.write(" ")
+            self.dispatch(t.value)
+
+    def _Pass(self, t):
+        self.fill("pass")
+
+    def _Break(self, t):
+        self.fill("break")
+
+    def _Continue(self, t):
+        self.fill("continue")
+
+    def _Delete(self, t):
+        self.fill("del ")
+        _interleave(lambda: self.write(", "), self.dispatch, t.targets)
+
+    def _Assert(self, t):
+        self.fill("assert ")
+        self.dispatch(t.test)
+        if t.msg:
+            self.write(", ")
+            self.dispatch(t.msg)
+
+    def _Global(self, t):
+        self.fill("global ")
+        _interleave(lambda: self.write(", "), self.write, t.names)
+
+    def _Nonlocal(self, t):
+        self.fill("nonlocal ")
+        _interleave(lambda: self.write(", "), self.write, t.names)
+
+    def _Await(self, t):
+        self.write("(")
+        self.write("await")
+        if t.value:
+            self.write(" ")
+            self.dispatch(t.value)
+        self.write(")")
+
+    def _Yield(self, t):
+        self.write("(")
+        self.write("yield")
+        if t.value:
+            self.write(" ")
+            self.dispatch(t.value)
+        self.write(")")
+
+    def _YieldFrom(self, t):
+        self.write("(")
+        self.write("yield from")
+        if t.value:
+            self.write(" ")
+            self.dispatch(t.value)
+        self.write(")")
+
+    def _Raise(self, t):
+        self.fill("raise")
+        if not t.exc:
+            assert not t.cause
+            return
+        self.write(" ")
+        self.dispatch(t.exc)
+        if t.cause:
+            self.write(" from ")
+            self.dispatch(t.cause)
+
+    def _Try(self, t):
+        self.fill("try")
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+        for ex in t.handlers:
+            self.dispatch(ex)
+        if t.orelse:
+            self.fill("else")
+            self.enter()
+            self.dispatch(t.orelse)
+            self.leave()
+        if t.finalbody:
+            self.fill("finally")
+            self.enter()
+            self.dispatch(t.finalbody)
+            self.leave()
+
+    def _ExceptHandler(self, t):
+        self.fill("except")
+        if t.type:
+            self.write(" ")
+            self.dispatch(t.type)
+        if t.name:
+            self.write(" as ")
+            self.write(t.name)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+
+    def _ClassDef(self, t):
+        self.write("\n")
+        for deco in t.decorator_list:
+            self.fill("@")
+            self.dispatch(deco)
+        self.fill("class " + t.name)
+        self.write("(")
+        comma = False
+        for e in t.bases:
+            if comma:
+                self.write(", ")
+            else:
+                comma = True
+            self.dispatch(e)
+        for e in t.keywords:
+            if comma:
+                self.write(", ")
+            else:
+                comma = True
+            self.dispatch(e)
+        self.write(")")
+
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+
+    def _FunctionDef(self, t):
+        self.__FunctionDef_helper(t, "def")
+
+    def _AsyncFunctionDef(self, t):
+        self.__FunctionDef_helper(t, "async def")
+
+    def __FunctionDef_helper(self, t, fill_suffix):
+        self.write("\n")
+        for deco in t.decorator_list:
+            self.fill("@")
+            self.dispatch(deco)
+        def_str = fill_suffix + " " + t.name + "("
+        self.fill(def_str)
+        self.dispatch(t.args)
+        self.write(")")
+        if t.returns:
+            self.write(" -> ")
+            self.dispatch(t.returns)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+
+    def _For(self, t):
+        self.__For_helper("for ", t)
+
+    def _AsyncFor(self, t):
+        self.__For_helper("async for ", t)
+
+    def __For_helper(self, fill, t):
+        self.fill(fill)
+        self.dispatch(t.target)
+        self.write(" in ")
+        self.dispatch(t.iter)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+        if t.orelse:
+            self.fill("else")
+            self.enter()
+            self.dispatch(t.orelse)
+            self.leave()
+
+    def _If(self, t):
+        self.fill("if ")
+        self.dispatch(t.test)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+        # collapse nested ifs into equivalent elifs.
+        while t.orelse and len(t.orelse) == 1 and isinstance(t.orelse[0], ast.If):
+            t = t.orelse[0]
+            self.fill("elif ")
+            self.dispatch(t.test)
+            self.enter()
+            self.dispatch(t.body)
+            self.leave()
+        # final else
+        if t.orelse:
+            self.fill("else")
+            self.enter()
+            self.dispatch(t.orelse)
+            self.leave()
+
+    def _While(self, t):
+        self.fill("while ")
+        self.dispatch(t.test)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+        if t.orelse:
+            self.fill("else")
+            self.enter()
+            self.dispatch(t.orelse)
+            self.leave()
+
+    def _With(self, t):
+        self.fill("with ")
+        _interleave(lambda: self.write(", "), self.dispatch, t.items)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+
+    def _AsyncWith(self, t):
+        self.fill("async with ")
+        _interleave(lambda: self.write(", "), self.dispatch, t.items)
+        self.enter()
+        self.dispatch(t.body)
+        self.leave()
+
+    # expr
+    def _JoinedStr(self, t):
+        self.write("f")
+        string = io.StringIO()
+        self._fstring_JoinedStr(t, string.write)
+        self.write(repr(string.getvalue()))
+
+    def _FormattedValue(self, t):
+        self.write("f")
+        string = io.StringIO()
+        self._fstring_FormattedValue(t, string.write)
+        self.write(repr(string.getvalue()))
+
+    def _fstring_JoinedStr(self, t, write):
+        for value in t.values:
+            meth = getattr(self, "_fstring_" + type(value).__name__)
+            meth(value, write)
+
+    def _fstring_Constant(self, t, write):
+        assert isinstance(t.value, str)
+        value = t.value.replace("{", "{{").replace("}", "}}")
+        write(value)
+
+    def _fstring_FormattedValue(self, t, write):
+        write("{")
+        expr = io.StringIO()
+        _Unparser(t.value, expr)
+        expr = expr.getvalue().rstrip("\n")
+        if expr.startswith("{"):
+            write(" ")  # Separate pair of opening brackets as "{ {"
+        write(expr)
+        if t.conversion != -1:
+            conversion = chr(t.conversion)
+            assert conversion in "sra"
+            write(f"!{conversion}")
+        if t.format_spec:
+            write(":")
+            meth = getattr(self, "_fstring_" + type(t.format_spec).__name__)
+            meth(t.format_spec, write)
+        write("}")
+
+    def _Name(self, t):
+        self.write(t.id)
+
+    def _write_constant(self, value):
+        if isinstance(value, (float, complex)):
+            # Substitute overflowing decimal literal for AST infinities.
+            self.write(repr(value).replace("inf", _INFSTR))
+        else:
+            self.write(repr(value))
+
+    def _Constant(self, t):
+        value = t.value
+        if isinstance(value, tuple):
+            self.write("(")
+            if len(value) == 1:
+                self._write_constant(value[0])
+                self.write(",")
+            else:
+                _interleave(lambda: self.write(", "), self._write_constant, value)
+            self.write(")")
+        elif value is ...:
+            self.write("...")
+        else:
+            if t.kind == "u":
+                self.write("u")
+            self._write_constant(t.value)
+
+    def _List(self, t):
+        self.write("[")
+        _interleave(lambda: self.write(", "), self.dispatch, t.elts)
+        self.write("]")
+
+    def _ListComp(self, t):
+        self.write("[")
+        self.dispatch(t.elt)
+        for gen in t.generators:
+            self.dispatch(gen)
+        self.write("]")
+
+    def _GeneratorExp(self, t):
+        self.write("(")
+        self.dispatch(t.elt)
+        for gen in t.generators:
+            self.dispatch(gen)
+        self.write(")")
+
+    def _SetComp(self, t):
+        self.write("{")
+        self.dispatch(t.elt)
+        for gen in t.generators:
+            self.dispatch(gen)
+        self.write("}")
+
+    def _DictComp(self, t):
+        self.write("{")
+        self.dispatch(t.key)
+        self.write(": ")
+        self.dispatch(t.value)
+        for gen in t.generators:
+            self.dispatch(gen)
+        self.write("}")
+
+    def _comprehension(self, t):
+        if t.is_async:
+            self.write(" async for ")
+        else:
+            self.write(" for ")
+        self.dispatch(t.target)
+        self.write(" in ")
+        self.dispatch(t.iter)
+        for if_clause in t.ifs:
+            self.write(" if ")
+            self.dispatch(if_clause)
+
+    def _IfExp(self, t):
+        self.write("(")
+        self.dispatch(t.body)
+        self.write(" if ")
+        self.dispatch(t.test)
+        self.write(" else ")
+        self.dispatch(t.orelse)
+        self.write(")")
+
+    def _Set(self, t):
+        assert t.elts  # should be at least one element
+        self.write("{")
+        _interleave(lambda: self.write(", "), self.dispatch, t.elts)
+        self.write("}")
+
+    def _Dict(self, t):
+        self.write("{")
+
+        def write_key_value_pair(k, v):
+            self.dispatch(k)
+            self.write(": ")
+            self.dispatch(v)
+
+        def write_item(item):
+            k, v = item
+            if k is None:
+                # for dictionary unpacking operator in dicts {**{'y': 2}}
+                # see PEP 448 for details
+                self.write("**")
+                self.dispatch(v)
+            else:
+                write_key_value_pair(k, v)
+
+        _interleave(lambda: self.write(", "), write_item, zip(t.keys, t.values))
+        self.write("}")
+
+    def _Tuple(self, t):
+        self.write("(")
+        if len(t.elts) == 1:
+            elt = t.elts[0]
+            self.dispatch(elt)
+            self.write(",")
+        else:
+            _interleave(lambda: self.write(", "), self.dispatch, t.elts)
+        self.write(")")
+
+    unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"}
+
+    def _UnaryOp(self, t):
+        self.write("(")
+        self.write(self.unop[t.op.__class__.__name__])
+        self.write(" ")
+        self.dispatch(t.operand)
+        self.write(")")
+
+    binop = {
+        "Add": "+",
+        "Sub": "-",
+        "Mult": "*",
+        "MatMult": "@",
+        "Div": "/",
+        "Mod": "%",
+        "LShift": "<<",
+        "RShift": ">>",
+        "BitOr": "|",
+        "BitXor": "^",
+        "BitAnd": "&",
+        "FloorDiv": "//",
+        "Pow": "**",
+    }
+
+    def _BinOp(self, t):
+        self.write("(")
+        self.dispatch(t.left)
+        self.write(" " + self.binop[t.op.__class__.__name__] + " ")
+        self.dispatch(t.right)
+        self.write(")")
+
+    cmpops = {
+        "Eq": "==",
+        "NotEq": "!=",
+        "Lt": "<",
+        "LtE": "<=",
+        "Gt": ">",
+        "GtE": ">=",
+        "Is": "is",
+        "IsNot": "is not",
+        "In": "in",
+        "NotIn": "not in",  # codespell:ignore
+    }
+
+    def _Compare(self, t):
+        self.write("(")
+        self.dispatch(t.left)
+        for o, e in zip(t.ops, t.comparators):
+            self.write(" " + self.cmpops[o.__class__.__name__] + " ")
+            self.dispatch(e)
+        self.write(")")
+
+    boolops = {ast.And: "and", ast.Or: "or"}
+
+    def _BoolOp(self, t):
+        self.write("(")
+        s = " %s " % self.boolops[t.op.__class__]
+        _interleave(lambda: self.write(s), self.dispatch, t.values)
+        self.write(")")
+
+    def _Attribute(self, t):
+        self.dispatch(t.value)
+        # Special case: 3.__abs__() is a syntax error, so if t.value
+        # is an integer literal then we need to either parenthesize
+        # it or add an extra space to get 3 .__abs__().
+        if isinstance(t.value, ast.Constant) and isinstance(t.value.value, int):
+            self.write(" ")
+        self.write(".")
+        self.write(t.attr)
+
+    def _Call(self, t):
+        self.dispatch(t.func)
+        self.write("(")
+        comma = False
+        for e in t.args:
+            if comma:
+                self.write(", ")
+            else:
+                comma = True
+            self.dispatch(e)
+        for e in t.keywords:
+            if comma:
+                self.write(", ")
+            else:
+                comma = True
+            self.dispatch(e)
+        self.write(")")
+
+    def _Subscript(self, t):
+        self.dispatch(t.value)
+        self.write("[")
+        if isinstance(t.slice, ast.Index) and isinstance(t.slice.value, ast.Tuple) and t.slice.value.elts:
+            if len(t.slice.value.elts) == 1:
+                elt = t.slice.value.elts[0]
+                self.dispatch(elt)
+                self.write(",")
+            else:
+                _interleave(lambda: self.write(", "), self.dispatch, t.slice.value.elts)
+        else:
+            self.dispatch(t.slice)
+        self.write("]")
+
+    def _Starred(self, t):
+        self.write("*")
+        self.dispatch(t.value)
+
+    # slice
+    def _Ellipsis(self, t):
+        self.write("...")
+
+    def _Index(self, t):
+        self.dispatch(t.value)
+
+    def _Slice(self, t):
+        if t.lower:
+            self.dispatch(t.lower)
+        self.write(":")
+        if t.upper:
+            self.dispatch(t.upper)
+        if t.step:
+            self.write(":")
+            self.dispatch(t.step)
+
+    def _ExtSlice(self, t):
+        if len(t.dims) == 1:
+            elt = t.dims[0]
+            self.dispatch(elt)
+            self.write(",")
+        else:
+            _interleave(lambda: self.write(", "), self.dispatch, t.dims)
+
+    # argument
+    def _arg(self, t):
+        self.write(t.arg)
+        if t.annotation:
+            self.write(": ")
+            self.dispatch(t.annotation)
+
+    # others
+    def _arguments(self, t):
+        first = True
+        # normal arguments
+        all_args = t.posonlyargs + t.args
+        defaults = [None] * (len(all_args) - len(t.defaults)) + t.defaults
+        for index, elements in enumerate(zip(all_args, defaults), 1):
+            a, d = elements
+            if first:
+                first = False
+            else:
+                self.write(", ")
+            self.dispatch(a)
+            if d:
+                self.write("=")
+                self.dispatch(d)
+            if index == len(t.posonlyargs):
+                self.write(", /")
+
+        # varargs, or bare '*' if no varargs but keyword-only arguments present
+        if t.vararg or t.kwonlyargs:
+            if first:
+                first = False
+            else:
+                self.write(", ")
+            self.write("*")
+            if t.vararg:
+                self.write(t.vararg.arg)
+                if t.vararg.annotation:
+                    self.write(": ")
+                    self.dispatch(t.vararg.annotation)
+
+        # keyword-only arguments
+        if t.kwonlyargs:
+            for a, d in zip(t.kwonlyargs, t.kw_defaults):
+                if first:
+                    first = False
+                else:
+                    self.write(", ")
+                (self.dispatch(a),)
+                if d:
+                    self.write("=")
+                    self.dispatch(d)
+
+        # kwargs
+        if t.kwarg:
+            if first:
+                first = False
+            else:
+                self.write(", ")
+            self.write("**" + t.kwarg.arg)
+            if t.kwarg.annotation:
+                self.write(": ")
+                self.dispatch(t.kwarg.annotation)
+
+    def _keyword(self, t):
+        if t.arg is None:
+            self.write("**")
+        else:
+            self.write(t.arg)
+            self.write("=")
+        self.dispatch(t.value)
+
+    def _Lambda(self, t):
+        self.write("(")
+        self.write("lambda ")
+        self.dispatch(t.args)
+        self.write(": ")
+        self.dispatch(t.body)
+        self.write(")")
+
+    def _alias(self, t):
+        self.write(t.name)
+        if t.asname:
+            self.write(" as " + t.asname)
+
+    def _withitem(self, t):
+        self.dispatch(t.context_expr)
+        if t.optional_vars:
+            self.write(" as ")
+            self.dispatch(t.optional_vars)

+ 97 - 59
taipy/gui/viselements.json

@@ -109,6 +109,12 @@
                         "type": "int",
                         "type": "int",
                         "default_value": "5",
                         "default_value": "5",
                         "doc": "The height of the displayed element if multiline is True."
                         "doc": "The height of the displayed element if multiline is True."
+                    },
+                    {
+                        "name": "type",
+                        "type": "str",
+                        "default_value": "\"text\"",
+                        "doc": "TODO: The type of input: text, tel, email ... as defined in HTML standards https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types "
                     }
                     }
                 ]
                 ]
             }
             }
@@ -133,6 +139,28 @@
                         "type": "str",
                         "type": "str",
                         "default_value": "None",
                         "default_value": "None",
                         "doc": "The label associated with the input."
                         "doc": "The label associated with the input."
+                    },
+                    {
+                        "name": "step",
+                        "type": "dynamic(int|float)",
+                        "default_value": "1",
+                        "doc": "The amount by which the value is incremented or decremented when the user clicks one of the arrow buttons."
+                    },
+                    {
+                        "name": "step_multiplier",
+                        "type": "dynamic(int|float)",
+                        "default_value": "10",
+                        "doc": "A factor that multiplies <i>step</i> when the user presses the Shift key while clicking one of the arrow buttons."
+                    },
+                    {
+                        "name": "min",
+                        "type": "dynamic(int|float)",
+                        "doc": "The minimum value to accept for this input."
+                    },
+                    {
+                        "name": "max",
+                        "type": "dynamic(int|float)",
+                        "doc": "The maximum value to accept for this input."
                     }
                     }
                 ]
                 ]
             }
             }
@@ -284,6 +312,16 @@
                         "name": "label",
                         "name": "label",
                         "type": "str",
                         "type": "str",
                         "doc": "The label associated with the input."
                         "doc": "The label associated with the input."
+                    },
+                    {
+                        "name": "min",
+                        "type": "dynamic(datetime)",
+                        "doc": "The minimum date to accept for this input."
+                    },
+                    {
+                        "name": "max",
+                        "type": "dynamic(datetime)",
+                        "doc": "The maximum date to accept for this input."
                     }
                     }
                 ]
                 ]
             }
             }
@@ -1513,65 +1551,6 @@
                 ]
                 ]
             }
             }
         ],
         ],
-        [
-            "dialog",
-            {
-                "inherits": [
-                    "partial",
-                    "active",
-                    "shared"
-                ],
-                "properties": [
-                    {
-                        "name": "open",
-                        "default_property": true,
-                        "type": "bool",
-                        "default_value": "False",
-                        "doc": "If True, the dialog is visible. If False, it is hidden."
-                    },
-                    {
-                        "name": "on_action",
-                        "type": "Callback",
-                        "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.</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": [
-                            [
-                                "state",
-                                "State"
-                            ],
-                            [
-                                "id",
-                                "str"
-                            ],
-                            [
-                                "payload",
-                                "dict"
-                            ]
-                        ]
-                    },
-                    {
-                        "name": "close_label",
-                        "type": "str",
-                        "default_value": "\"Close\"",
-                        "doc": "The tooltip of the top-right close icon button. In the <tt>on_action</tt> callback, args will hold -1."
-                    },
-                    {
-                        "name": "labels",
-                        "type": " str|list[str]",
-                        "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the <tt>on_action</tt> callback (that index is -1 for the <i>close</i> icon)."
-                    },
-                    {
-                        "name": "width",
-                        "type": "str|int|float",
-                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
-                    },
-                    {
-                        "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
-                    }
-                ]
-            }
-        ],
         [
         [
             "tree",
             "tree",
             {
             {
@@ -1669,6 +1648,65 @@
                 ]
                 ]
             }
             }
         ],
         ],
+        [
+            "dialog",
+            {
+                "inherits": [
+                    "partial",
+                    "active",
+                    "shared"
+                ],
+                "properties": [
+                    {
+                        "name": "open",
+                        "default_property": true,
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If True, the dialog is visible. If False, it is hidden."
+                    },
+                    {
+                        "name": "on_action",
+                        "type": "Callback",
+                        "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.</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": [
+                            [
+                                "state",
+                                "State"
+                            ],
+                            [
+                                "id",
+                                "str"
+                            ],
+                            [
+                                "payload",
+                                "dict"
+                            ]
+                        ]
+                    },
+                    {
+                        "name": "close_label",
+                        "type": "str",
+                        "default_value": "\"Close\"",
+                        "doc": "The tooltip of the top-right close icon button. In the <tt>on_action</tt> callback, args will hold -1."
+                    },
+                    {
+                        "name": "labels",
+                        "type": " str|list[str]",
+                        "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the <tt>on_action</tt> callback (that index is -1 for the <i>close</i> icon)."
+                    },
+                    {
+                        "name": "width",
+                        "type": "str|int|float",
+                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
+                    },
+                    {
+                        "name": "height",
+                        "type": "str|int|float",
+                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
+                    }
+                ]
+            }
+        ],
         [
         [
             "layout",
             "layout",
             {
             {

+ 2 - 1
taipy/gui_core/_context.py

@@ -125,6 +125,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.JOB:
         elif event.entity_type == EventEntityType.JOB:
             with self.lock:
             with self.lock:
                 self.jobs_list = None
                 self.jobs_list = None
+            self.broadcast_core_changed({"jobs": event.entity_id})
         elif event.entity_type == EventEntityType.SUBMISSION:
         elif event.entity_type == EventEntityType.SUBMISSION:
             self.submission_status_callback(event.entity_id, event)
             self.submission_status_callback(event.entity_id, event)
         elif event.entity_type == EventEntityType.DATA_NODE:
         elif event.entity_type == EventEntityType.DATA_NODE:
@@ -181,7 +182,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         # callback
                         # callback
                         submission_name = submission.properties.get("on_submission")
                         submission_name = submission.properties.get("on_submission")
                         if submission_name:
                         if submission_name:
-                            self.gui._call_user_callback(
+                            self.gui.invoke_callback(
                                 client_id,
                                 client_id,
                                 submission_name,
                                 submission_name,
                                 [
                                 [

+ 18 - 18
tests/core/_entity/test_ready_to_run_property.py

@@ -14,7 +14,7 @@ from taipy import ScenarioId, SequenceId, TaskId
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
 from taipy.config.config import Config
 from taipy.config.config import Config
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
-from taipy.core.reason.reason import Reasons
+from taipy.core.reason import DataNodeEditInProgress, DataNodeIsNotWritten, ReasonCollection
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
@@ -33,7 +33,7 @@ def test_scenario_without_input_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -46,7 +46,7 @@ def test_scenario_submittable_with_inputs_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -61,7 +61,7 @@ def test_scenario_submittable_even_with_output_not_ready_to_run():
     dn_3 = scenario.dn_3
     dn_3 = scenario.dn_3
 
 
     assert not dn_3.is_ready_for_reading
     assert not dn_3.is_ready_for_reading
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -78,7 +78,7 @@ def test_scenario_not_submittable_not_in_property_because_it_is_lazy():
     assert dn_1.is_ready_for_reading
     assert dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     # Since it is a lazy property, the scenario and the datanodes is not yet in the dictionary
     # Since it is a lazy property, the scenario and the datanodes is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
@@ -97,14 +97,14 @@ def test_scenario_not_submittable_if_one_input_edit_in_progress():
 
 
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
-        f"DataNode {dn_1.id} is being edited"
+        DataNodeEditInProgress(dn_1.id)
     }
     }
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons == f"DataNode {dn_1.id} is being edited."
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons == f"DataNode {dn_1.id} is being edited."
 
 
@@ -125,7 +125,7 @@ def test_scenario_not_submittable_for_multiple_reasons():
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
@@ -134,12 +134,12 @@ def test_scenario_not_submittable_for_multiple_reasons():
     assert dn_2.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_2.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
-        f"DataNode {dn_1.id} is being edited"
+        DataNodeEditInProgress(dn_1.id)
     }
     }
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_2.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_2.id]
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_2.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_2.id] == {
-        f"DataNode {dn_2.id} is being edited",
-        f"DataNode {dn_2.id} is not written",
+        DataNodeEditInProgress(dn_2.id),
+        DataNodeIsNotWritten(dn_2.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     assert f"DataNode {dn_2.id} is being edited" in reason_str
     assert f"DataNode {dn_2.id} is being edited" in reason_str
@@ -156,14 +156,14 @@ def test_writing_input_remove_reasons():
 
 
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     # Since it is a lazy property, the scenario is not yet in the dictionary
     # Since it is a lazy property, the scenario is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
     dn_1.lock_edit()
     dn_1.lock_edit()
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
-        f"DataNode {dn_1.id} is being edited",
-        f"DataNode {dn_1.id} is not written",
+        DataNodeEditInProgress(dn_1.id),
+        DataNodeIsNotWritten(dn_1.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     assert f"DataNode {dn_1.id} is being edited" in reason_str
     assert f"DataNode {dn_1.id} is being edited" in reason_str
@@ -171,7 +171,7 @@ def test_writing_input_remove_reasons():
 
 
     dn_1.write(10)
     dn_1.write(10)
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
-    assert isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
 
 
@@ -188,8 +188,8 @@ def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager,
 
 
     dn.lock_edit()
     dn.lock_edit()
     assert _ReadyToRunProperty._submittable_id_datanodes[entity.id]._reasons[dn.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[entity.id]._reasons[dn.id] == {
-        f"DataNode {dn.id} is being edited",
-        f"DataNode {dn.id} is not written",
+        DataNodeEditInProgress(dn.id),
+        DataNodeIsNotWritten(dn.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[entity.id].reasons
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[entity.id].reasons
     assert f"DataNode {dn.id} is being edited" in reason_str
     assert f"DataNode {dn.id} is being edited" in reason_str
@@ -197,7 +197,7 @@ def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager,
 
 
     dn.write("ANY VALUE")
     dn.write("ANY VALUE")
     assert manager._is_submittable(entity)
     assert manager._is_submittable(entity)
-    assert isinstance(manager._is_submittable(entity), Reasons)
+    assert isinstance(manager._is_submittable(entity), ReasonCollection)
     assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
 
 

+ 45 - 43
tests/core/common/test_reason.py

@@ -9,61 +9,63 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-from taipy.core.reason.reason import Reasons
+from taipy.core.reason import ReasonCollection
 
 
 
 
 def test_create_reason():
 def test_create_reason():
-    reason = Reasons("entity_id")
-    assert reason.entity_id == "entity_id"
-    assert reason._reasons == {}
-    assert reason
-    assert not reason._entity_id_exists_in_reason("entity_id")
-    assert reason.reasons == ""
+    reason_collection = ReasonCollection()
+    assert reason_collection._reasons == {}
+    assert reason_collection
+    assert not reason_collection._entity_id_exists_in_reason("entity_id")
+    assert reason_collection.reasons == ""
 
 
 
 
 def test_add_and_remove_reason():
 def test_add_and_remove_reason():
-    reason = Reasons("entity_id")
-    reason._add_reason("entity_id_1", "Some reason")
-    assert reason._reasons == {"entity_id_1": {"Some reason"}}
-    assert not reason
-    assert reason._entity_id_exists_in_reason("entity_id_1")
-    assert reason.reasons == "Some reason."
+    reason_collection = ReasonCollection()
+    reason_collection._add_reason("entity_id_1", "Some reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Some reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection.reasons == "Some reason."
 
 
-    reason._add_reason("entity_id_1", "Another reason")
-    reason._add_reason("entity_id_2", "Some more reason")
-    assert reason._reasons == {"entity_id_1": {"Some reason", "Another reason"}, "entity_id_2": {"Some more reason"}}
-    assert not reason
-    assert reason._entity_id_exists_in_reason("entity_id_1")
-    assert reason._entity_id_exists_in_reason("entity_id_2")
+    reason_collection._add_reason("entity_id_1", "Another reason")
+    reason_collection._add_reason("entity_id_2", "Some more reason")
+    assert reason_collection._reasons == {
+        "entity_id_1": {"Some reason", "Another reason"},
+        "entity_id_2": {"Some more reason"},
+    }
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection._entity_id_exists_in_reason("entity_id_2")
 
 
-    reason._remove_reason("entity_id_1", "Some reason")
-    assert reason._reasons == {"entity_id_1": {"Another reason"}, "entity_id_2": {"Some more reason"}}
-    assert not reason
-    assert reason._entity_id_exists_in_reason("entity_id_1")
-    assert reason._entity_id_exists_in_reason("entity_id_2")
+    reason_collection._remove_reason("entity_id_1", "Some reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Another reason"}, "entity_id_2": {"Some more reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection._entity_id_exists_in_reason("entity_id_2")
 
 
-    reason._remove_reason("entity_id_2", "Some more reason")
-    assert reason._reasons == {"entity_id_1": {"Another reason"}}
-    assert not reason
-    assert reason._entity_id_exists_in_reason("entity_id_1")
-    assert not reason._entity_id_exists_in_reason("entity_id_2")
+    reason_collection._remove_reason("entity_id_2", "Some more reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Another reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert not reason_collection._entity_id_exists_in_reason("entity_id_2")
 
 
-    reason._remove_reason("entity_id_1", "Another reason")
-    assert reason._reasons == {}
-    assert reason
-    assert not reason._entity_id_exists_in_reason("entity_id_1")
+    reason_collection._remove_reason("entity_id_1", "Another reason")
+    assert reason_collection._reasons == {}
+    assert reason_collection
+    assert not reason_collection._entity_id_exists_in_reason("entity_id_1")
 
 
 
 
 def test_get_reason_string_from_reason():
 def test_get_reason_string_from_reason():
-    reason = Reasons("entity_id")
-    reason._add_reason("entity_id_1", "Some reason")
-    assert reason.reasons == "Some reason."
+    reason_collection = ReasonCollection()
+    reason_collection._add_reason("entity_id_1", "Some reason")
+    assert reason_collection.reasons == "Some reason."
 
 
-    reason._add_reason("entity_id_2", "Some more reason")
-    assert reason.reasons == "Some reason; Some more reason."
+    reason_collection._add_reason("entity_id_2", "Some more reason")
+    assert reason_collection.reasons == "Some reason; Some more reason."
 
 
-    reason._add_reason("entity_id_1", "Another reason")
-    assert reason.reasons.count(";") == 2
-    assert "Some reason" in reason.reasons
-    assert "Another reason" in reason.reasons
-    assert "Some more reason" in reason.reasons
+    reason_collection._add_reason("entity_id_1", "Another reason")
+    assert reason_collection.reasons.count(";") == 2
+    assert "Some reason" in reason_collection.reasons
+    assert "Another reason" in reason_collection.reasons
+    assert "Some more reason" in reason_collection.reasons

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

@@ -24,6 +24,7 @@ from taipy.core.data.data_node_id import DataNodeId
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.exceptions.exceptions import InvalidDataNodeType, ModelNotFound
 from taipy.core.exceptions.exceptions import InvalidDataNodeType, ModelNotFound
+from taipy.core.reason import NotGlobalScope, WrongConfigType
 from tests.core.utils.named_temporary_file import NamedTemporaryFile
 from tests.core.utils.named_temporary_file import NamedTemporaryFile
 
 
 
 
@@ -61,11 +62,16 @@ class TestDataManager:
 
 
         reasons = _DataManager._can_create(dn_config)
         reasons = _DataManager._can_create(dn_config)
         assert bool(reasons) is False
         assert bool(reasons) is False
-        assert reasons._reasons == {dn_config.id: {'Data node config "dn" does not have GLOBAL scope'}}
+        assert reasons._reasons[dn_config.id] == {NotGlobalScope(dn_config.id)}
+        assert (
+            str(list(reasons._reasons[dn_config.id])[0])
+            == f'Data node config "{dn_config.id}" does not have GLOBAL scope'
+        )
 
 
         reasons = _DataManager._can_create(1)
         reasons = _DataManager._can_create(1)
         assert bool(reasons) is False
         assert bool(reasons) is False
-        assert reasons._reasons == {"1": {'Object "1" must be a valid DataNodeConfig'}}
+        assert reasons._reasons["1"] == {WrongConfigType("1", DataNodeConfig.__name__)}
+        assert str(list(reasons._reasons["1"])[0]) == 'Object "1" must be a valid DataNodeConfig'
 
 
     def test_create_data_node_with_name_provided(self):
     def test_create_data_node_with_name_provided(self):
         dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb")
         dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb")

+ 59 - 2
tests/core/scenario/test_scenario_manager.py

@@ -13,6 +13,7 @@ from datetime import datetime, timedelta
 from typing import Callable, Iterable, Optional
 from typing import Callable, Iterable, Optional
 from unittest.mock import ANY, patch
 from unittest.mock import ANY, patch
 
 
+import freezegun
 import pytest
 import pytest
 
 
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
@@ -24,6 +25,7 @@ from taipy.core._orchestrator._orchestrator import _Orchestrator
 from taipy.core._version._version_manager import _VersionManager
 from taipy.core._version._version_manager import _VersionManager
 from taipy.core.common import _utils
 from taipy.core.common import _utils
 from taipy.core.common._utils import _Subscriber
 from taipy.core.common._utils import _Subscriber
+from taipy.core.config.scenario_config import ScenarioConfig
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.in_memory import InMemoryDataNode
@@ -39,6 +41,7 @@ from taipy.core.exceptions.exceptions import (
     UnauthorizedTagError,
     UnauthorizedTagError,
 )
 )
 from taipy.core.job._job_manager import _JobManager
 from taipy.core.job._job_manager import _JobManager
+from taipy.core.reason import WrongConfigType
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario import Scenario
@@ -381,13 +384,15 @@ def test_can_create():
 
 
     reasons = _ScenarioManager._can_create(task_config)
     reasons = _ScenarioManager._can_create(task_config)
     assert bool(reasons) is False
     assert bool(reasons) is False
-    assert reasons._reasons == {task_config.id: {'Object "task" must be a valid ScenarioConfig'}}
+    assert reasons._reasons[task_config.id] == {WrongConfigType(task_config.id, ScenarioConfig.__name__)}
+    assert str(list(reasons._reasons[task_config.id])[0]) == 'Object "task" must be a valid ScenarioConfig'
     with pytest.raises(AttributeError):
     with pytest.raises(AttributeError):
         _ScenarioManager._create(task_config)
         _ScenarioManager._create(task_config)
 
 
     reasons = _ScenarioManager._can_create(1)
     reasons = _ScenarioManager._can_create(1)
     assert bool(reasons) is False
     assert bool(reasons) is False
-    assert reasons._reasons == {"1": {'Object "1" must be a valid ScenarioConfig'}}
+    assert reasons._reasons["1"] == {WrongConfigType(1, ScenarioConfig.__name__)}
+    assert str(list(reasons._reasons["1"])[0]) == 'Object "1" must be a valid ScenarioConfig'
     with pytest.raises(AttributeError):
     with pytest.raises(AttributeError):
         _ScenarioManager._create(1)
         _ScenarioManager._create(1)
 
 
@@ -1481,3 +1486,55 @@ def test_get_scenarios_by_config_id_in_multiple_versions_environment():
 
 
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
+
+
+def test_filter_scenarios_by_creation_datetime():
+    scenario_config_1 = Config.configure_scenario("s1", sequence_configs=[])
+
+    with freezegun.freeze_time("2024-01-01"):
+        s_1_1 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-01-03"):
+        s_1_2 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-02-01"):
+        s_1_3 = _ScenarioManager._create(scenario_config_1)
+
+    all_scenarios = _ScenarioManager._get_all()
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios
+
+    # The time period is inclusive
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 3),
+    )
+    assert len(filtered_scenarios) == 2
+    assert sorted([s_1_1.id, s_1_2.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2023, 1, 1),
+        created_end_time=datetime(2025, 1, 1),
+    )
+    assert len(filtered_scenarios) == 3
+    assert sorted([s_1_1.id, s_1_2.id, s_1_3.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 2, 1),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_3] == filtered_scenarios
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios

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

@@ -11,6 +11,7 @@
 
 
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 
 
+import freezegun
 import pytest
 import pytest
 
 
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
@@ -435,3 +436,55 @@ def test_get_scenarios_by_config_id_in_multiple_versions_environment(init_sql_re
 
 
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_1.id)) == 3
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
     assert len(_ScenarioManager._get_by_config_id(scenario_config_2.id)) == 2
+
+
+def test_filter_scenarios_by_creation_datetime(init_sql_repo):
+    scenario_config_1 = Config.configure_scenario("s1", sequence_configs=[])
+
+    with freezegun.freeze_time("2024-01-01"):
+        s_1_1 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-01-03"):
+        s_1_2 = _ScenarioManager._create(scenario_config_1)
+    with freezegun.freeze_time("2024-02-01"):
+        s_1_3 = _ScenarioManager._create(scenario_config_1)
+
+    all_scenarios = _ScenarioManager._get_all()
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios
+
+    # The time period is inclusive
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 1, 1),
+        created_end_time=datetime(2024, 1, 3),
+    )
+    assert len(filtered_scenarios) == 2
+    assert sorted([s_1_1.id, s_1_2.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2023, 1, 1),
+        created_end_time=datetime(2025, 1, 1),
+    )
+    assert len(filtered_scenarios) == 3
+    assert sorted([s_1_1.id, s_1_2.id, s_1_3.id]) == sorted([scenario.id for scenario in filtered_scenarios])
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_start_time=datetime(2024, 2, 1),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_3] == filtered_scenarios
+
+    filtered_scenarios = _ScenarioManager._filter_by_creation_time(
+        scenarios=all_scenarios,
+        created_end_time=datetime(2024, 1, 2),
+    )
+    assert len(filtered_scenarios) == 1
+    assert [s_1_1] == filtered_scenarios

+ 6 - 0
tests/core/test_taipy.py

@@ -431,6 +431,9 @@ class TestTaipy:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_all_by_tag") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_all_by_tag") as mck:
             tp.get_scenarios(tag="tag")
             tp.get_scenarios(tag="tag")
             mck.assert_called_once_with("tag")
             mck.assert_called_once_with("tag")
+        with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._filter_by_creation_time") as mck:
+            tp.get_scenarios(created_start_time=datetime.datetime(2021, 1, 1))
+            mck.assert_called_once_with([], datetime.datetime(2021, 1, 1), None)
 
 
     def test_get_scenarios_sorted(self):
     def test_get_scenarios_sorted(self):
         scenario_1_cfg = Config.configure_scenario(id="scenario_1")
         scenario_1_cfg = Config.configure_scenario(id="scenario_1")
@@ -500,6 +503,9 @@ class TestTaipy:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_primary_scenarios") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get_primary_scenarios") as mck:
             tp.get_primary_scenarios()
             tp.get_primary_scenarios()
             mck.assert_called_once_with()
             mck.assert_called_once_with()
+        with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._filter_by_creation_time") as mck:
+            tp.get_scenarios(created_end_time=datetime.datetime(2021, 1, 1))
+            mck.assert_called_once_with([], None, datetime.datetime(2021, 1, 1))
 
 
     def test_set_primary(self, scenario):
     def test_set_primary(self, scenario):
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._set_primary") as mck:
         with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._set_primary") as mck:

+ 46 - 5
tests/gui/actions/test_invoke_callback.py

@@ -9,11 +9,20 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
+import contextlib
 import inspect
 import inspect
 
 
 from flask import g
 from flask import g
 
 
-from taipy.gui import Gui, Markdown, State, invoke_callback
+from taipy.gui import Gui, Markdown, State
+
+
+@contextlib.contextmanager
+def get_state(gui: Gui, state_id: str):
+    with gui.get_flask_app().app_context():
+        client_id = gui._bindings()._get_or_create_scope(state_id)[0]
+        gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
+        yield gui._Gui__state  # type: ignore[attr-defined]
 
 
 
 
 def test_invoke_callback(gui: Gui, helpers):
 def test_invoke_callback(gui: Gui, helpers):
@@ -29,13 +38,45 @@ def test_invoke_callback(gui: Gui, helpers):
     gui._set_frame(inspect.currentframe())
     gui._set_frame(inspect.currentframe())
 
 
     gui.add_page("test", Markdown("<|Hello {name}|button|id={btn_id}|>\n<|{val}|>"))
     gui.add_page("test", Markdown("<|Hello {name}|button|id={btn_id}|>\n<|{val}|>"))
-    gui.run(run_server=False, single_client=True)
+    gui.run(run_server=False)
     flask_client = gui._server.test_client()
     flask_client = gui._server.test_client()
     # client id
     # client id
     cid = helpers.create_scope_and_get_sid(gui)
     cid = helpers.create_scope_and_get_sid(gui)
+    # Get the jsx once so that the page will be evaluated -> variable will be registered
+    flask_client.get(f"/taipy-jsx/test?client_id={cid}")
+
+    gui.invoke_callback(cid, user_callback, [])
+    with get_state(gui, cid) as state:
+        assert state.val == 10
+
+
+def test_invoke_callback_sid(gui: Gui, helpers):
+    name = "World!"  # noqa: F841
+    btn_id = "button1"  # noqa: F841
+
+    val = 1  # noqa: F841
+
+    def user_callback(state: State):
+        state.val = 10
+
+    # set gui frame
+    gui._set_frame(inspect.currentframe())
+
+    gui.add_page("test", Markdown("<|Hello {name}|button|id={btn_id}|>\n<|{val}|>"))
+    gui.run(run_server=False)
+    flask_client = gui._server.test_client()
+    # client id
+    cid = helpers.create_scope_and_get_sid(gui)
+    base_sid, _ = gui._bindings()._get_or_create_scope("base sid")
+
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
     with gui.get_flask_app().app_context():
     with gui.get_flask_app().app_context():
-        g.client_id = cid
-        invoke_callback(gui, cid, user_callback, [])
-        assert gui._Gui__state.val == 10  # type: ignore[attr-defined]
+        g.client_id = base_sid
+        gui.invoke_callback(cid, user_callback, [])
+        assert g.client_id == base_sid
+
+    with get_state(gui, base_sid) as base_state:
+        assert base_state.val == 1
+    with get_state(gui, cid) as a_state:
+        assert a_state.val == 10

+ 10 - 0
tests/gui/builder/control/test_dialog.py

@@ -74,3 +74,13 @@ def test_dialog_labels_builder(gui: Gui, helpers):
         "open={_TpB_tpec_TpExPr_dialog_open_TPMDL_0}",
         "open={_TpB_tpec_TpExPr_dialog_open_TPMDL_0}",
     ]
     ]
     helpers.test_control_builder(gui, page, expected_list)
     helpers.test_control_builder(gui, page, expected_list)
+
+def test_dialog_builder_block(gui: Gui, helpers):
+    with tgb.dialog(title="Another Dialog") as content:  # type: ignore[attr-defined]
+        tgb.text(value="This is in a dialog")  # type: ignore[attr-defined]
+    expected_list = [
+        "<Dialog",
+        'title="Another Dialog"',
+        "This is in a dialog",
+    ]
+    helpers.test_control_builder(gui, tgb.Page(content, frame=None), expected_list)

+ 33 - 0
tests/gui/builder/test_lambda.py

@@ -0,0 +1,33 @@
+# 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 taipy.gui.builder as tgb
+from taipy.gui import Gui
+
+
+def test_builder_lambda(gui: Gui, test_client, helpers):
+    message = {"a": "value A", "b": "value B"}
+    gui._bind_var_val("message", message)
+    with tgb.Page(frame=None) as page:
+        for key in message:
+            tgb.text(lambda message: str(message.get(key)))  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['defaultValue="value A"', 'defaultValue="value B"']
+    helpers.test_control_builder(gui, page, expected_list)
+
+
+def test_builder_lambda_2(gui: Gui, test_client, helpers):
+    message = {"a": "value A", "b": "value B"}
+    gui._bind_var_val("message", message)
+    with tgb.Page(frame=None) as page:
+        for key in message:
+            tgb.text(lambda message: str(message.get(key)), mode=lambda message: "mode " + str(message.get(key)))  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['defaultValue="value A"', 'defaultValue="value B"', 'mode="mode value A"', 'mode="mode value B"']
+    helpers.test_control_builder(gui, page, expected_list)

+ 30 - 0
tests/gui/builder/test_on_action.py

@@ -0,0 +1,30 @@
+# 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 taipy.gui.builder as tgb
+from taipy.gui import Gui, notify
+
+
+def test_builder_on_function(gui: Gui, test_client, helpers):
+    def on_slider(state):
+        notify(state, "success", f"Value: {state.value}")
+    gui._bind_var_val("on_slider", on_slider)
+    with tgb.Page(frame=None) as page:
+        tgb.slider(value="{value}", on_change=on_slider)  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['<Slider','onChange="on_slider"']
+    helpers.test_control_builder(gui, page, expected_list)
+
+
+def test_builder_on_lambda(gui: Gui, test_client, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.slider(value="{value}", on_change=lambda s: notify(s, "success", f"Lambda Value: {s.value}"))  # type: ignore[attr-defined] # noqa: B023
+    expected_list = ['<Slider','onChange="__lambda_']
+    helpers.test_control_builder(gui, page, expected_list)

+ 138 - 0
tests/gui/gui_specific/test_broadcast.py

@@ -0,0 +1,138 @@
+# 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 contextlib
+import inspect
+
+import pytest
+
+from taipy.gui import Gui
+
+
+@contextlib.contextmanager
+def get_state(gui: Gui, state_id: str):
+    with gui.get_flask_app().app_context():
+        client_id = gui._bindings()._get_or_create_scope(state_id)[0]
+        gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
+        yield gui._Gui__state  # type: ignore[attr-defined]
+
+def test_multiple_scopes(gui: Gui):
+    var = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("page1", "<|{var}|>")
+    gui.run(run_server=False)
+    with get_state(gui, "s1") as state1:
+        assert state1.var == 1
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+    with get_state(gui, "s1") as state1:
+        state1.var = 2
+        assert state1.var == 2
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+
+
+def test_shared_variable(gui: Gui):
+    var = 1  # noqa: F841
+    s_var = 10  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", "<|{var}|>")
+    gui.add_shared_variable("s_var")
+    gui.run(run_server=False)
+    with get_state(gui, "s1") as state1:
+        assert state1.var == 1
+        assert state1.s_var == 10
+        state1.var = 2
+        state1.s_var = 20
+        assert state1.var == 2
+        assert state1.s_var == 20
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+        assert state1.s_var == 20
+    Gui._clear_shared_variable()
+
+
+def test_broadcast_change(gui: Gui):
+    # Bind test variables
+    v1 = "none"  # noqa: F841
+    v2 = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", " <|{v1}|><|{v2}|>")
+    gui.run(run_server=False)
+    s1, _ = gui._bindings()._get_or_create_scope("s1")
+    s2, _ = gui._bindings()._get_or_create_scope("s2")
+    gui.broadcast_change("v2", 2)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "none"
+        assert state1.v2 == 2
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "none"
+        assert state2.v2 == 2
+
+def test_broadcast_changes(gui: Gui):
+    # Bind test variables
+    v1 = "none"  # noqa: F841
+    v2 = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", " <|{v1}|><|{v2}|>")
+    gui.run(run_server=False)
+    s1, _ = gui._bindings()._get_or_create_scope("s1")
+    s2, _ = gui._bindings()._get_or_create_scope("s2")
+
+    changes = { "v1": "some", "v2": 2}
+    gui.broadcast_changes(changes)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "some"
+        assert state1.v2 == 2
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "some"
+        assert state2.v2 == 2
+
+    gui.broadcast_changes(v1="more", v2=3)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "more"
+        assert state1.v2 == 3
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "more"
+        assert state2.v2 == 3
+
+    gui.broadcast_changes({ "v1": "more yet"}, v2=4)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "more yet"
+        assert state1.v2 == 4
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "more yet"
+        assert state2.v2 == 4
+
+
+def test_broadcast_callback(gui: Gui):
+    gui.run(run_server=False)
+
+    res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
+    assert isinstance(res, dict)
+    assert not res
+
+    gui._bindings()._get_or_create_scope("test scope")
+
+    res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
+    assert len(res) == 1
+    assert res.get("test scope", None) == "Hello World"
+
+    with pytest.warns(UserWarning):
+        res = gui.broadcast_callback(print, ["Hello World"], "mine")
+        assert isinstance(res, dict)
+        assert res.get("test scope", "Hello World") is None
+
+    gui._bindings()._get_or_create_scope("another scope")
+    res = gui.broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
+    assert len(res) == 2
+    assert res.get("test scope", None) == "Hello World"
+    assert res.get("another scope", None) == "Hello World"

+ 0 - 12
tests/gui/gui_specific/test_gui.py

@@ -42,18 +42,6 @@ def test__get_user_instance(gui: Gui):
             gui._get_user_instance("", type(None))
             gui._get_user_instance("", type(None))
 
 
 
 
-def test__call_broadcast_callback(gui: Gui):
-    gui.run(run_server=False)
-    with gui.get_flask_app().app_context():
-        res = gui._call_broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
-        assert res == "Hello World"
-
-    with gui.get_flask_app().app_context():
-        with pytest.warns(UserWarning):
-            res = gui._call_broadcast_callback(print, ["Hello World"], "mine")
-            assert res is None
-
-
 def test__refresh_expr(gui: Gui):
 def test__refresh_expr(gui: Gui):
     gui.run(run_server=False)
     gui.run(run_server=False)
     with gui.get_flask_app().app_context():
     with gui.get_flask_app().app_context():

+ 6 - 2
tests/gui/gui_specific/test_shared.py

@@ -13,9 +13,13 @@ from taipy.gui import Gui
 
 
 
 
 def test_add_shared_variables(gui: Gui):
 def test_add_shared_variables(gui: Gui):
+    assert len(gui._Gui__shared_variables) == 0  # type: ignore[attr-defined]
+
     Gui.add_shared_variable("var1", "var2")
     Gui.add_shared_variable("var1", "var2")
     assert isinstance(gui._Gui__shared_variables, list)  # type: ignore[attr-defined]
     assert isinstance(gui._Gui__shared_variables, list)  # type: ignore[attr-defined]
     assert len(gui._Gui__shared_variables) == 2  # type: ignore[attr-defined]
     assert len(gui._Gui__shared_variables) == 2  # type: ignore[attr-defined]
 
 
-    Gui.add_shared_variables("var1", "var2")
-    assert len(gui._Gui__shared_variables) == 2  # type: ignore[attr-defined]
+    Gui.add_shared_variables("var1", "var3")
+    assert len(gui._Gui__shared_variables) == 3  # type: ignore[attr-defined]
+
+    Gui._clear_shared_variable()

+ 6 - 6
tests/gui_core/test_context_is_submitable.py

@@ -14,7 +14,7 @@ from unittest.mock import Mock, patch
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 from taipy.core import Job, JobId, Scenario, Task
 from taipy.core import Job, JobId, Scenario, Task
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
-from taipy.core.reason.reason import Reasons
+from taipy.core.reason import ReasonCollection
 from taipy.gui_core._context import _GuiCoreContext
 from taipy.gui_core._context import _GuiCoreContext
 
 
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
@@ -25,13 +25,13 @@ a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 
 
 
 
 def mock_is_submittable_reason(entity_id):
 def mock_is_submittable_reason(entity_id):
-    reason = Reasons(entity_id)
-    reason._add_reason(entity_id, "a reason")
-    return reason
+    reasons = ReasonCollection()
+    reasons._add_reason(entity_id, "a reason")
+    return reasons
 
 
 
 
-def mock_has_no_reason(entity_id):
-    return Reasons(entity_id)
+def mock_has_no_reason():
+    return ReasonCollection()
 
 
 
 
 def mock_core_get(entity_id):
 def mock_core_get(entity_id):

+ 12 - 4
tools/gui/generate_pyi.py

@@ -110,7 +110,7 @@ def build_doc(name: str, element: t.Dict[str, t.Any]):
         r"\1\3",
         r"\1\3",
         doc,
         doc,
     )
     )
-    doc = markdownify(doc, strip=['br'])
+    doc = markdownify(doc, strip=["br"])
     return f"{element['name']} ({element['type']}): {doc} {'(default: '+markdownify(element['default_value']) + ')' if 'default_value' in element else ''}"  # noqa: E501
     return f"{element['name']} ({element['type']}): {doc} {'(default: '+markdownify(element['default_value']) + ')' if 'default_value' in element else ''}"  # noqa: E501
 
 
 
 
@@ -118,16 +118,24 @@ for control_element in viselements["controls"]:
     name = control_element[0]
     name = control_element[0]
     property_list: t.List[t.Dict[str, t.Any]] = []
     property_list: t.List[t.Dict[str, t.Any]] = []
     property_names: t.List[str] = []
     property_names: t.List[str] = []
+    hidden_properties: t.List[str] = []
     for property in get_properties(control_element[1], viselements):
     for property in get_properties(control_element[1], viselements):
-        if property["name"] not in property_names and "[" not in property["name"]:
+        if "hide" in property and property["hide"] is True:
+            hidden_properties.append(property["name"])
+            continue
+        if (
+            property["name"] not in property_names
+            and "[" not in property["name"]
+            and property["name"] not in hidden_properties
+        ):
             if "default_property" in property and property["default_property"] is True:
             if "default_property" in property and property["default_property"] is True:
                 property_list.insert(0, property)
                 property_list.insert(0, property)
                 property_names.insert(0, property["name"])
                 property_names.insert(0, property["name"])
                 continue
                 continue
             property_list.append(property)
             property_list.append(property)
             property_names.append(property["name"])
             property_names.append(property["name"])
-    properties = ", ".join([f"{p} = ..." for p in property_names])
-    doc_arguments = "\n".join([build_doc(name, p) for p in property_list])
+    properties = ", ".join([f"{p} = ..." for p in property_names if p not in hidden_properties])
+    doc_arguments = "\n".join([build_doc(name, p) for p in property_list if p["name"] not in hidden_properties])
     # append properties to __init__.pyi
     # append properties to __init__.pyi
     with open(builder_pyi_file, "a") as file:
     with open(builder_pyi_file, "a") as file:
         file.write(
         file.write(

+ 2 - 0
tools/release/fetch_latest_versions.py

@@ -35,6 +35,8 @@ def fetch_latest_releases_from_github(dev=False, target_version="", target_packa
             releases["rest"] = releases.get("rest") or tag.split("-")[0]
             releases["rest"] = releases.get("rest") or tag.split("-")[0]
         elif "templates" in tag:
         elif "templates" in tag:
             releases["templates"] = releases.get("templates") or tag.split("-")[0]
             releases["templates"] = releases.get("templates") or tag.split("-")[0]
+        elif "-" not in tag:
+            releases["taipy"] = releases.get("taipy") or tag
     releases[target_package] = target_version
     releases[target_package] = target_version
     return releases
     return releases
 
 

Некоторые файлы не были показаны из-за большого количества измененных файлов