Browse Source

Merge branch 'feature/#1429-number-step-attribute' of github.com:Avaiga/taipy into feature/#1429-number-step-attribute

namnguyen 11 tháng trước cách đây
mục cha
commit
7134f6a960

+ 7 - 5
.github/workflows/frontend.yml

@@ -59,13 +59,15 @@ jobs:
       - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
         run: npm run build --if-present
 
-      - run: npm test
+      - if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        run: npm test
 
       - name: Code coverage
-        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request'
-        uses: artiomtr/jest-coverage-report-action@v2.2.6
+        if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-fe-build.outputs.cache-hit != 'true'
+        uses: artiomtr/jest-coverage-report-action@v2.3.0
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
           threshold: "80"
-          working-directory: "frontend/taipy-gui"
-          skip-step: "install"
+          working-directory: "./frontend/taipy-gui"
+          skip-step: install
+          annotations: failed-tests

+ 1 - 0
Pipfile

@@ -42,6 +42,7 @@ numpy = "<2.0.0"
 freezegun = "*"
 ipython = "*"
 ipykernel = "*"
+markdownify = "*"
 mkdocs = "*"
 mkdocs-autorefs = "*"
 mkdocs-include-markdown-plugin = "*"

BIN
frontend/taipy-gui/public/favicon.ico


BIN
frontend/taipy-gui/public/favicon.png


+ 1 - 1
frontend/taipy-gui/public/index.html

@@ -5,7 +5,7 @@
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link rel="manifest" href="manifest.json" />
-    <link rel="icon" type="image/png" href="{{favicon}}" />
+    <link rel="icon" type="image/png" href="favicon.png" class="taipy-favicon" data-url="{{favicon}}" />
     <link rel="apple-touch-icon" href="favicon.png" />
     <title>{{title}}</title>
     <script>{%- if config -%}

+ 4 - 1
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -22,7 +22,7 @@ import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
 import { getBaseURL, TIMEZONE_CLIENT } from "../utils";
 import { parseData } from "../utils/dataFormat";
 import { MenuProps } from "../utils/lov";
-import { getLocalStorageValue, IdMessage, storeClientId } from "./utils";
+import { changeFavicon, getLocalStorageValue, IdMessage, storeClientId } from "./utils";
 import { ligthenPayload, sendWsMessage, TAIPY_CLIENT_ID, WsMessage } from "./wsUtils";
 
 enum Types {
@@ -228,6 +228,8 @@ const messageToAction = (message: WsMessage) => {
             return createPartialAction((message as unknown as Record<string, string>).name, true);
         } else if (message.type === "ACK") {
             return createAckAction((message as unknown as IdMessage).id);
+        } else if (message.type === "FV") {
+            changeFavicon((message.payload as Record<string, string>)?.value);
         }
     }
     return {} as TaipyBaseAction;
@@ -278,6 +280,7 @@ export const initializeWebSocket = (socket: Socket | undefined, dispatch: Dispat
         socket.on("message", getWsMessageListener(dispatch));
         // only now does the socket tries to open/connect
         socket.connect();
+        changeFavicon();
     }
 };
 

+ 20 - 0
frontend/taipy-gui/src/context/utils.ts

@@ -1,3 +1,4 @@
+import axios from "axios";
 import { TAIPY_CLIENT_ID } from "./wsUtils";
 
 export const getLocalStorageValue = <T = string>(key: string, defaultValue: T, values?: T[]) => {
@@ -10,3 +11,22 @@ export const storeClientId = (id: string) => localStorage && localStorage.setIte
 export interface IdMessage {
     id: string;
 }
+
+export const changeFavicon = (url?: string) => {
+    const link: HTMLLinkElement | null = document.querySelector("link.taipy-favicon");
+    if (link) {
+        const { url: taipyUrl } = link.dataset;
+        const fetchUrl = url || (taipyUrl as string);
+        axios
+            .get(fetchUrl)
+            .then(() => {
+                link.href = fetchUrl;
+            })
+            .catch((error) => {
+                if (fetchUrl !== taipyUrl) {
+                    link.href = taipyUrl as string;
+                }
+                console.log(error);
+            });
+    }
+};

+ 2 - 1
frontend/taipy-gui/src/context/wsUtils.ts

@@ -20,7 +20,8 @@ export type WsMessageType =
     | "GMC"
     | "GDT"
     | "AID"
-    | "GR";
+    | "GR"
+    | "FV";
 
 export interface WsMessage {
     type: WsMessageType;

+ 5 - 5
taipy/config/common/_template_handler.py

@@ -51,15 +51,15 @@ class _TemplateHandler:
                 if required:
                     raise MissingEnvVariableError(f"Environment variable {var} is not set.")
                 return default
-            if type == bool:
+            if type is bool:
                 return cls._to_bool(val)
-            elif type == int:
+            elif type is int:
                 return cls._to_int(val)
-            elif type == float:
+            elif type is float:
                 return cls._to_float(val)
-            elif type == Scope:
+            elif type is Scope:
                 return cls._to_scope(val)
-            elif type == Frequency:
+            elif type is Frequency:
                 return cls._to_frequency(val)
             else:
                 if dynamic_type == "bool":

+ 44 - 41
taipy/core/_entity/_migrate/_migrate_sql.py

@@ -13,6 +13,7 @@ import json
 import os
 import shutil
 import sqlite3
+from contextlib import closing
 from typing import Dict, Tuple
 
 from taipy.logger._taipy_logger import _TaipyLogger
@@ -24,32 +25,34 @@ __logger = _TaipyLogger._get_logger()
 
 def _load_all_entities_from_sql(db_file: str) -> Tuple[Dict, Dict]:
     conn = sqlite3.connect(db_file)
-    query = "SELECT model_id, document FROM taipy_model"
-    query_version = "SELECT * FROM taipy_version"
-    cursor = conn.execute(query)
-    entities = {}
-    versions = {}
-
-    for row in cursor:
-        _id = row[0]
-        document = row[1]
-        entities[_id] = {"data": json.loads(document)}
-
-    cursor = conn.execute(query_version)
-    for row in cursor:
-        id = row[0]
-        config_id = row[1]
-        creation_date = row[2]
-        is_production = row[3]
-        is_development = row[4]
-        is_latest = row[5]
-        versions[id] = {
-            "config_id": config_id,
-            "creation_date": creation_date,
-            "is_production": is_production,
-            "is_development": is_development,
-            "is_latest": is_latest,
-        }
+    with closing(conn):
+        query = "SELECT model_id, document FROM taipy_model"
+        query_version = "SELECT * FROM taipy_version"
+        cursor = conn.execute(query)
+        entities = {}
+        versions = {}
+
+        for row in cursor:
+            _id = row[0]
+            document = row[1]
+            entities[_id] = {"data": json.loads(document)}
+
+        cursor = conn.execute(query_version)
+        for row in cursor:
+            id = row[0]
+            config_id = row[1]
+            creation_date = row[2]
+            is_production = row[3]
+            is_development = row[4]
+            is_latest = row[5]
+            versions[id] = {
+                "config_id": config_id,
+                "creation_date": creation_date,
+                "is_production": is_production,
+                "is_development": is_development,
+                "is_latest": is_latest,
+            }
+
     return entities, versions
 
 
@@ -123,21 +126,21 @@ def __insert_version(version: dict, conn):
 
 def __write_entities_to_sql(_entities: Dict, _versions: Dict, db_file: str):
     conn = sqlite3.connect(db_file)
-
-    for k, entity in _entities.items():
-        if "SCENARIO" in k:
-            __insert_scenario(entity["data"], conn)
-        elif "TASK" in k:
-            __insert_task(entity["data"], conn)
-        elif "DATANODE" in k:
-            __insert_datanode(entity["data"], conn)
-        elif "JOB" in k:
-            __insert_job(entity["data"], conn)
-        elif "CYCLE" in k:
-            __insert_cycle(entity["data"], conn)
-
-    for _, version in _versions.items():
-        __insert_version(version, conn)
+    with closing(conn):
+        for k, entity in _entities.items():
+            if "SCENARIO" in k:
+                __insert_scenario(entity["data"], conn)
+            elif "TASK" in k:
+                __insert_task(entity["data"], conn)
+            elif "DATANODE" in k:
+                __insert_datanode(entity["data"], conn)
+            elif "JOB" in k:
+                __insert_job(entity["data"], conn)
+            elif "CYCLE" in k:
+                __insert_cycle(entity["data"], conn)
+
+        for _, version in _versions.items():
+            __insert_version(version, conn)
 
 
 def _restore_migrate_sql_entities(path: str) -> bool:

+ 4 - 4
taipy/gui/data/array_dict_data_accessor.py

@@ -33,18 +33,18 @@ class _ArrayDictDataAccessor(_PandasDataAccessor):
             types = {type(x) for x in value}
             if len(types) == 1:
                 type_elt = next(iter(types), None)
-                if type_elt == list:
+                if type_elt is list:
                     lengths = {len(x) for x in value}
                     return (
                         pd.DataFrame(value)
                         if len(lengths) == 1
                         else [pd.DataFrame({f"{i}/0": v}) for i, v in enumerate(value)]
                     )
-                elif type_elt == dict:
+                elif type_elt is dict:
                     return [pd.DataFrame(v) for v in value]
-                elif type_elt == _MapDict:
+                elif type_elt is _MapDict:
                     return [pd.DataFrame(v._dict) for v in value]
-                elif type_elt == pd.DataFrame:
+                elif type_elt is pd.DataFrame:
                     return value
 
             elif len(types) == 2 and list in types and pd.DataFrame in types:

+ 45 - 15
taipy/gui/gui.py

@@ -223,6 +223,7 @@ class Gui:
     __ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
     __DOWNLOAD_ACTION = "__Taipy__download_csv"
     __DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"
+    __DEFAULT_FAVICON_URL = "https://raw.githubusercontent.com/Avaiga/taipy-assets/develop/favicon.png"
 
     __RE_HTML = re.compile(r"(.*?)\.html$")
     __RE_MD = re.compile(r"(.*?)\.md$")
@@ -322,6 +323,7 @@ class Gui:
         self.__evaluator: _Evaluator = None  # type: ignore
         self.__adapter = _Adapter()
         self.__directory_name_of_pages: t.List[str] = []
+        self.__favicon: t.Optional[t.Union[str, Path]] = None
 
         # default actions
         self.on_action: t.Optional[t.Callable] = None
@@ -1117,9 +1119,7 @@ class Gui:
             # Get Module Context
             if mc := self._get_page_context(page_path):
                 page_renderer = self._get_page(page_path)._renderer
-                self._bind_custom_page_variables(
-                    page_renderer, self._get_client_id()
-                )
+                self._bind_custom_page_variables(page_renderer, self._get_client_id())
                 # get metadata if there is one
                 metadata: t.Dict[str, t.Any] = {}
                 if hasattr(page_renderer, "_metadata"):
@@ -1227,7 +1227,7 @@ class Gui:
     def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
         try:
             to = list(self.__get_sids(client_id)) if client_id else []
-            self._server._ws.emit("message", payload, to=to if to else None)
+            self._server._ws.emit("message", payload, to=to if to else None, include_self=True)
             time.sleep(0.001)
         except Exception as e:  # pragma: no cover
             _warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
@@ -1315,9 +1315,19 @@ class Gui:
         else:
             self.__send_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})
 
-    def __send_ws_broadcast(self, var_name: str, var_value: t.Any, client_id: t.Optional[str] = None):
+    def __send_ws_broadcast(
+        self,
+        var_name: str,
+        var_value: t.Any,
+        client_id: t.Optional[str] = None,
+        message_type: t.Optional[_WsType] = None,
+    ):
         self.__broadcast_ws(
-            {"type": _WsType.UPDATE.value, "name": _get_broadcast_var_name(var_name), "payload": {"value": var_value}},
+            {
+                "type": _WsType.UPDATE.value if message_type is None else message_type.value,
+                "name": _get_broadcast_var_name(var_name),
+                "payload": {"value": var_value},
+            },
             client_id,
         )
 
@@ -1977,7 +1987,13 @@ class Gui:
     def load_config(self, config: Config) -> None:
         self._config._load(config)
 
-    def _broadcast(self, name: str, value: t.Any, client_id: t.Optional[str] = None):
+    def _broadcast(
+        self,
+        name: str,
+        value: t.Any,
+        client_id: t.Optional[str] = None,
+        message_type: t.Optional[_WsType] = None,
+    ):
         """NOT DOCUMENTED
         Send the new value of a variable to all connected clients.
 
@@ -1986,7 +2002,7 @@ class Gui:
             value: The value (must be serializable to the JSON format).
             client_id: The client id (broadcast to all client if None)
         """
-        self.__send_ws_broadcast(name, value, client_id)
+        self.__send_ws_broadcast(name, value, client_id, message_type)
 
     def _broadcast_all_clients(self, name: str, value: t.Any):
         try:
@@ -2411,7 +2427,7 @@ class Gui:
                 static_folder=_webapp_path,
                 template_folder=_webapp_path,
                 title=self._get_config("title", "Taipy App"),
-                favicon=self._get_config("favicon", "favicon.png"),
+                favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
                 root_margin=self._get_config("margin", None),
                 scripts=scripts,
                 styles=styles,
@@ -2440,8 +2456,7 @@ class Gui:
         async_mode: str = "gevent",
         **kwargs,
     ) -> t.Optional[Flask]:
-        """
-        Start the server that delivers pages to web clients.
+        """Start the server that delivers pages to web clients.
 
         Once you enter `run()`, users can run web browsers and point to the web server
         URL that `Gui` serves. The default is to listen to the *localhost* address
@@ -2593,8 +2608,7 @@ class Gui:
         )
 
     def reload(self):  # pragma: no cover
-        """
-        Reload the web server.
+        """Reload the web server.
 
         This function reloads the underlying web server only in the situation where
         it was run in a separated thread: the *run_in_thread* parameter to the
@@ -2607,8 +2621,7 @@ class Gui:
             _TaipyLogger._get_logger().info("Gui server has been reloaded.")
 
     def stop(self):
-        """
-        Stop the web server.
+        """Stop the web server.
 
         This function stops the underlying web server only in the situation where
         it was run in a separated thread: the *run_in_thread* parameter to the
@@ -2621,3 +2634,20 @@ class Gui:
 
     def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
         return contextlib.nullcontext()
+
+    def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
+        """Change the favicon for all clients.
+
+        This function dynamically changes the favicon of Taipy GUI pages for all connected client.
+        favicon_path can be an URL (relative or not) or a file path.
+        TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
+         the favicon when the application starts.
+
+        """
+        if state or self.__favicon != favicon_path:
+            if not state:
+                self.__favicon = favicon_path
+            url = self._get_content("__taipy_favicon", favicon_path, True)
+            self._broadcast(
+                "taipy_favicon", url, self._get_client_id() if state else None, message_type=_WsType.FAVICON
+            )

+ 1 - 1
taipy/gui/server.py

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

+ 12 - 0
taipy/gui/state.py

@@ -13,6 +13,7 @@ import inspect
 import typing as t
 from contextlib import nullcontext
 from operator import attrgetter
+from pathlib import Path
 from types import FrameType
 
 from flask import has_app_context
@@ -83,6 +84,7 @@ class State:
         "broadcast",
         "get_gui",
         "refresh",
+        "set_favicon",
         "_set_context",
         "_notebook_context",
         "_get_placeholder",
@@ -243,3 +245,13 @@ class State:
 
     def __exit__(self, exc_type, exc_value, traceback):
         return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)
+
+    def set_favicon(self, favicon_path: t.Union[str, Path]):
+        """Change the favicon for the client of this state.
+
+        This function dynamically changes the favicon of Taipy GUI pages for a specific client.
+        favicon_path can be an URL (relative or not) or a file path.
+        TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
+         the favicon when the application starts.
+        """
+        super().__getattribute__(State.__gui_attr).set_favicon(favicon_path, self)

+ 1 - 0
taipy/gui/types.py

@@ -50,6 +50,7 @@ class _WsType(Enum):
     GET_MODULE_CONTEXT = "GMC"
     GET_DATA_TREE = "GDT"
     GET_ROUTES = "GR"
+    FAVICON = "FV"
 
 
 NumberTypes = {"int", "int64", "float", "float64"}

+ 1 - 0
taipy/gui_core/_context.py

@@ -125,6 +125,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.JOB:
             with self.lock:
                 self.jobs_list = None
+            self.broadcast_core_changed({"jobs": event.entity_id})
         elif event.entity_type == EventEntityType.SUBMISSION:
             self.submission_status_callback(event.entity_id, event)
         elif event.entity_type == EventEntityType.DATA_NODE:

+ 0 - 2
tests/core/_entity/test_migrate_cli.py

@@ -12,7 +12,6 @@
 import filecmp
 import os
 import shutil
-import sys
 from sqlite3 import OperationalError
 from unittest.mock import patch
 
@@ -207,7 +206,6 @@ def test_migrate_sql_backup_and_remove(caplog, tmp_sqlite):
     assert not os.path.exists(backup_sqlite)
 
 
-@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on windows due to PermissionError: [WinError 32]")
 def test_migrate_sql_backup_and_restore(caplog, tmp_sqlite):
     _MigrateCLI.create_parser()
 

+ 9 - 9
tests/core/config/test_file_config.py

@@ -193,9 +193,9 @@ def test_read_configuration_file():
     Config.override(file_config.filename)
 
     assert len(Config.data_nodes) == 4
-    assert type(Config.data_nodes["my_datanode"]) == DataNodeConfig
-    assert type(Config.data_nodes["my_datanode2"]) == DataNodeConfig
-    assert type(Config.data_nodes["my_datanode3"]) == DataNodeConfig
+    assert type(Config.data_nodes["my_datanode"]) is DataNodeConfig
+    assert type(Config.data_nodes["my_datanode2"]) is DataNodeConfig
+    assert type(Config.data_nodes["my_datanode3"]) is DataNodeConfig
     assert Config.data_nodes["my_datanode"].path == "/data/csv"
     assert Config.data_nodes["my_datanode2"].path == "/data2/csv"
     assert Config.data_nodes["my_datanode3"].path == "/data3/csv"
@@ -206,27 +206,27 @@ def test_read_configuration_file():
     assert Config.data_nodes["my_datanode3"].source == "local"
 
     assert len(Config.tasks) == 2
-    assert type(Config.tasks["my_task"]) == TaskConfig
+    assert type(Config.tasks["my_task"]) is TaskConfig
     assert Config.tasks["my_task"].id == "my_task"
     assert Config.tasks["my_task"].description == "task description"
     assert Config.tasks["my_task"].function == print
     assert len(Config.tasks["my_task"].inputs) == 1
-    assert type(Config.tasks["my_task"].inputs[0]) == DataNodeConfig
+    assert type(Config.tasks["my_task"].inputs[0]) is DataNodeConfig
     assert Config.tasks["my_task"].inputs[0].path == "/data/csv"
     assert Config.tasks["my_task"].inputs[0].id == "my_datanode"
     assert len(Config.tasks["my_task"].outputs) == 1
-    assert type(Config.tasks["my_task"].outputs[0]) == DataNodeConfig
+    assert type(Config.tasks["my_task"].outputs[0]) is DataNodeConfig
     assert Config.tasks["my_task"].outputs[0].path == "/data2/csv"
     assert Config.tasks["my_task"].outputs[0].id == "my_datanode2"
 
     assert len(Config.scenarios) == 2
-    assert type(Config.scenarios["my_scenario"]) == ScenarioConfig
+    assert type(Config.scenarios["my_scenario"]) is ScenarioConfig
     assert Config.scenarios["my_scenario"].id == "my_scenario"
     assert Config.scenarios["my_scenario"].owner == "John Doe"
     assert len(Config.scenarios["my_scenario"].tasks) == 1
-    assert type(Config.scenarios["my_scenario"].tasks[0]) == TaskConfig
+    assert type(Config.scenarios["my_scenario"].tasks[0]) is TaskConfig
     assert len(Config.scenarios["my_scenario"].additional_data_nodes) == 1
-    assert type(Config.scenarios["my_scenario"].additional_data_nodes[0]) == DataNodeConfig
+    assert type(Config.scenarios["my_scenario"].additional_data_nodes[0]) is DataNodeConfig
     assert Config.scenarios["my_scenario"].tasks[0].id == "my_task"
     assert Config.scenarios["my_scenario"].tasks[0].description == "task description"
     assert Config.scenarios["my_scenario"].additional_data_nodes[0].id == "my_datanode3"

+ 35 - 0
tests/gui/gui_specific/test_favicon.py

@@ -0,0 +1,35 @@
+# 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 inspect
+import warnings
+
+from taipy.gui import Gui, Markdown
+
+
+def test_favicon(gui: Gui, helpers):
+
+    with warnings.catch_warnings(record=True):
+        gui._set_frame(inspect.currentframe())
+        gui.add_page("test", Markdown("#This is a page"))
+        gui.run(run_server=False)
+        client = gui._server.test_client()
+        # WS client and emit
+        ws_client = gui._server._ws.test_client(gui._server.get_flask())
+        # Get the jsx once so that the page will be evaluated -> variable will be registered
+        sid = helpers.create_scope_and_get_sid(gui)
+        client.get(f"/taipy-jsx/test/?client_id={sid}")
+        gui.set_favicon("https://newfavicon.com/favicon.png")
+        # assert for received message (message that would be sent to the front-end client)
+        msgs = ws_client.get_received()
+        assert msgs
+        assert msgs[0].get("args", {}).get("type", None) == "FV"
+        assert msgs[0].get("args", {}).get("payload", {}).get("value", None) == "https://newfavicon.com/favicon.png"

+ 2 - 3
tools/gui/builder/block.txt

@@ -2,8 +2,7 @@
 class {{name}}(_Block):
     _ELEMENT_NAME: str
     def __init__(self, {{properties}}) -> None:
-        """
-        Arguments:
-            {{doc_arguments}}
+        """### Arguments:
+{{doc_arguments}}
         """
         ...

+ 2 - 3
tools/gui/builder/control.txt

@@ -2,8 +2,7 @@
 class {{name}}(_Control):
     _ELEMENT_NAME: str
     def __init__(self, {{properties}}) -> None:
-        """
-        Arguments:
-            {{doc_arguments}}
+        """### Arguments:
+{{doc_arguments}}
         """
         ...

+ 31 - 4
tools/gui/generate_pyi.py

@@ -11,8 +11,11 @@
 
 import json
 import os
+import re
 import typing as t
 
+from markdownify import markdownify
+
 # ############################################################
 # Generate Python interface definition files
 # ############################################################
@@ -46,6 +49,19 @@ with open(gui_pyi_file, "r") as file:
 with open(gui_pyi_file, "w") as write_file:
     write_file.write(replaced_content)
 
+# ################
+# Read the version
+# ################
+current_version = "latest"
+with open("./taipy/gui/version.json", "r") as vfile:
+    version = json.load(vfile)
+    if "dev" in version.get("ext", ""):
+        current_version = "develop"
+    else:
+        current_version = f'release-{version.get("major", 0)}.{version.get("minor", 0)}'
+taipy_doc_url = f"https://docs.taipy.io/en/{current_version}/manuals/gui/viselements/"
+
+
 # ############################################################
 # Generate Page Builder pyi file (gui/builder/__init__.pyi)
 # ############################################################
@@ -80,11 +96,22 @@ def get_properties(element, viselements) -> t.List[t.Dict[str, t.Any]]:
     return properties
 
 
-def build_doc(element: t.Dict[str, t.Any]):
+def build_doc(name: str, element: t.Dict[str, t.Any]):
     if "doc" not in element:
         return ""
     doc = str(element["doc"]).replace("\n", f'\n{16*" "}')
-    return f"{element['name']} ({element['type']}): {doc} {'(default: '+element['default_value'] + ')' if 'default_value' in element else ''}"  # noqa: E501
+    doc = re.sub(
+        r"^(.*\..*\shref=\")([^h].*)(\".*\..*)$",
+        r"\1" + taipy_doc_url + name + r"/\2\3",
+        doc,
+    )
+    doc = re.sub(
+        r"^(.*\.)(<br/>|\s)(See below((?!href=).)*\.)(.*)$",
+        r"\1\3",
+        doc,
+    )
+    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
 
 
 for control_element in viselements["controls"]:
@@ -100,7 +127,7 @@ for control_element in viselements["controls"]:
             property_list.append(property)
             property_names.append(property["name"])
     properties = ", ".join([f"{p} = ..." for p in property_names])
-    doc_arguments = f"\n{12*' '}".join([build_doc(p) for p in property_list])
+    doc_arguments = "\n".join([build_doc(name, p) for p in property_list])
     # append properties to __init__.pyi
     with open(builder_pyi_file, "a") as file:
         file.write(
@@ -118,7 +145,7 @@ for block_element in viselements["blocks"]:
             property_list.append(property)
             property_names.append(property["name"])
     properties = ", ".join([f"{p} = ..." for p in property_names])
-    doc_arguments = f"{8*' '}".join([build_doc(p) for p in property_list])
+    doc_arguments = "\n".join([build_doc(name, p) for p in property_list])
     # append properties to __init__.pyi
     with open(builder_pyi_file, "a") as file:
         file.write(