浏览代码

Merge branch 'develop' into 1703-bug-number-visual-element-does-not-update-when-using-arrow-to-change-value

Nam Nguyen 8 月之前
父节点
当前提交
11ece35dae
共有 59 个文件被更改,包括 1045 次插入344 次删除
  1. 5 1
      .github/workflows/build-and-release.yml
  2. 22 23
      README.md
  3. 2 2
      frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
  4. 25 8
      frontend/taipy-gui/src/components/Taipy/Button.spec.tsx
  5. 13 3
      frontend/taipy-gui/src/components/Taipy/Button.tsx
  6. 1 1
      frontend/taipy-gui/src/components/Taipy/Chat.tsx
  7. 21 3
      frontend/taipy-gui/src/components/Taipy/DateRange.spec.tsx
  8. 30 12
      frontend/taipy-gui/src/components/Taipy/DateRange.tsx
  9. 28 2
      frontend/taipy-gui/src/components/Taipy/DateSelector.spec.tsx
  10. 11 5
      frontend/taipy-gui/src/components/Taipy/DateSelector.tsx
  11. 28 14
      frontend/taipy-gui/src/components/Taipy/Field.spec.tsx
  12. 19 4
      frontend/taipy-gui/src/components/Taipy/Field.tsx
  13. 44 19
      frontend/taipy-gui/src/components/Taipy/FileDownload.spec.tsx
  14. 12 2
      frontend/taipy-gui/src/components/Taipy/FileDownload.tsx
  15. 19 9
      frontend/taipy-gui/src/components/Taipy/FileSelector.spec.tsx
  16. 7 4
      frontend/taipy-gui/src/components/Taipy/FileSelector.tsx
  17. 2 3
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
  18. 69 31
      frontend/taipy-gui/src/components/Taipy/ThemeToggle.spec.tsx
  19. 17 6
      frontend/taipy-gui/src/components/Taipy/ThemeToggle.tsx
  20. 37 8
      frontend/taipy-gui/src/components/Taipy/Toggle.spec.tsx
  21. 22 9
      frontend/taipy-gui/src/components/Taipy/Toggle.tsx
  22. 4 6
      frontend/taipy-gui/src/components/Taipy/utils.ts
  23. 4 2
      frontend/taipy-gui/src/utils/ErrorBoundary.tsx
  24. 35 30
      frontend/taipy/src/CoreSelector.tsx
  25. 20 46
      frontend/taipy/src/DataNodeViewer.tsx
  26. 114 14
      frontend/taipy/src/JobSelector.tsx
  27. 186 0
      frontend/taipy/src/JobViewer.tsx
  28. 20 16
      frontend/taipy/src/utils.ts
  29. 0 3
      taipy/config/pyproject.toml
  30. 1 1
      taipy/config/version.json
  31. 0 3
      taipy/core/pyproject.toml
  32. 1 1
      taipy/core/version.json
  33. 1 1
      taipy/gui/_renderers/builder.py
  34. 7 0
      taipy/gui/_renderers/factory.py
  35. 0 3
      taipy/gui/pyproject.toml
  36. 1 1
      taipy/gui/version.json
  37. 65 23
      taipy/gui/viselements.json
  38. 9 1
      taipy/gui_core/_GuiCoreLib.py
  39. 26 1
      taipy/gui_core/_context.py
  40. 27 7
      taipy/gui_core/viselements.json
  41. 0 3
      taipy/rest/pyproject.toml
  42. 1 1
      taipy/rest/version.json
  43. 0 3
      taipy/templates/pyproject.toml
  44. 1 1
      taipy/templates/version.json
  45. 1 1
      taipy/version.json
  46. 8 0
      tests/gui/control/test_button.py
  47. 13 0
      tests/gui/control/test_date.py
  48. 15 0
      tests/gui/control/test_date_range.py
  49. 12 0
      tests/gui/control/test_file_download.py
  50. 13 0
      tests/gui/control/test_file_selector.py
  51. 6 0
      tests/gui/control/test_number.py
  52. 7 0
      tests/gui/control/test_text.py
  53. 6 0
      tests/gui/control/test_toggle.py
  54. 1 1
      tools/packages/taipy-config/setup.py
  55. 1 1
      tools/packages/taipy-core/setup.py
  56. 2 2
      tools/packages/taipy-gui/setup.py
  57. 1 1
      tools/packages/taipy-rest/setup.py
  58. 1 1
      tools/packages/taipy-templates/setup.py
  59. 1 1
      tools/release/build_package_structure.py

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

@@ -171,7 +171,7 @@ jobs:
 
   build-and-release-taipy:
     runs-on: ubuntu-latest
-    needs: [build-and-release-taipy-packages, fetch-versions ]
+    needs: [build-and-release-taipy-packages, fetch-versions]
     timeout-minutes: 20
     steps:
       - uses: actions/checkout@v4
@@ -212,6 +212,10 @@ jobs:
         run: |
           cp -r tools/packages/taipy/. .
 
+      - name: Build Frontend
+        run: |
+          python tools/frontend/bundle_build.py
+
       - name: Build Taipy package
         run: |
           python -m build

+ 22 - 23
README.md

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

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

@@ -247,7 +247,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                 setOrder(isAsc ? "desc" : "asc");
                 setOrderBy(col);
                 setRows([]);
-                setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
+                Promise.resolve().then(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true)); // So that the state can be changed
             }
         },
         [orderBy, order]
@@ -256,7 +256,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
     useEffect(() => {
         if (refresh) {
             setRows([]);
-            setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
+            Promise.resolve().then(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true)); // So that the state can be changed
         }
     }, [refresh]);
 

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

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

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

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

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

@@ -329,7 +329,7 @@ const Chat = (props: ChatProps) => {
 
     useEffect(() => {
         if (refresh) {
-            setTimeout(() => loadMoreItems(0), 1); // So that the state can be changed
+            Promise.resolve().then(() => loadMoreItems(0)); // So that the state can be changed
         }
     }, [refresh, loadMoreItems]);
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -563,11 +563,10 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                 {rows.map((row, index) => {
                                     const sel = selected.indexOf(index + startIndex);
                                     if (sel == 0) {
-                                        setTimeout(
+                                        Promise.resolve().then(
                                             () =>
                                                 selectedRowRef.current?.scrollIntoView &&
-                                                selectedRowRef.current.scrollIntoView({ block: "center" }),
-                                            1
+                                                selectedRowRef.current.scrollIntoView({ block: "center" })
                                         );
                                     }
                                     return (

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 114 - 14
frontend/taipy/src/JobSelector.tsx

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

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

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

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

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

+ 0 - 3
taipy/config/pyproject.toml

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

+ 1 - 1
taipy/config/version.json

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

+ 0 - 3
taipy/core/pyproject.toml

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

+ 1 - 1
taipy/core/version.json

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

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

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

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

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

+ 0 - 3
taipy/gui/pyproject.toml

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

+ 1 - 1
taipy/gui/version.json

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

+ 65 - 23
taipy/gui/viselements.json

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

+ 9 - 1
taipy/gui_core/_GuiCoreLib.py

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

+ 26 - 1
taipy/gui_core/_context.py

@@ -9,6 +9,7 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 
+import datetime
 import json
 import typing as t
 from collections import defaultdict
@@ -29,6 +30,7 @@ from taipy.core import (
     DataNode,
     DataNodeId,
     Job,
+    JobId,
     Scenario,
     ScenarioId,
     Sequence,
@@ -806,8 +808,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         job.id,
                         job.get_simple_label(),
                         [],
-                        entity.get_simple_label() if entity else "",
                         entity.id if entity else "",
+                        entity.get_simple_label() if entity else "",
                         job.submit_id,
                         job.creation_date,
                         job.status.value,
@@ -855,6 +857,29 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         errs.append(f"Error canceling job. {e}")
             _GuiCoreContext.__assign_var(state, payload.get("error_id"), "<br/>".join(errs) if errs else "")
 
+    def get_job_details(self, job_id: t.Optional[JobId]):
+        try:
+            if job_id and is_readable(job_id) and (job := core_get(job_id)) is not None:
+                if isinstance(job, Job):
+                    entity = core_get(job.owner_id)
+                    return (
+                        job.id,
+                        job.get_simple_label(),
+                        entity.id if entity else "",
+                        entity.get_simple_label() if entity else "",
+                        job.submit_id,
+                        job.creation_date,
+                        job.status.value,
+                        _get_reason(is_deletable(job)),
+                        ""
+                        if job.execution_duration is None
+                        else str(datetime.timedelta(seconds=job.execution_duration)),
+                        [] if job.stacktrace is None else job.stacktrace,
+                    )
+        except Exception as e:
+            _warn(f"Access to job ({job.id if hasattr(job, 'id') else 'No_id'}) failed", e)
+        return None
+
     def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
         self.__lazy_start()
         args = payload.get("args")

+ 27 - 7
taipy/gui_core/viselements.json

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

+ 0 - 3
taipy/rest/pyproject.toml

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

+ 1 - 1
taipy/rest/version.json

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

+ 0 - 3
taipy/templates/pyproject.toml

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

+ 1 - 1
taipy/templates/version.json

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

+ 1 - 1
taipy/version.json

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
tools/packages/taipy-config/setup.py

@@ -43,6 +43,6 @@ setup(
         ]
     ),
     include_package_data=True,
-    data_files=[('version', ['version.json'])],
+    data_files=[('version', [version_path])],
     tests_require=test_requirements,
 )

+ 1 - 1
tools/packages/taipy-core/setup.py

@@ -46,7 +46,7 @@ setup(
     install_requires=requirements,
     packages=find_packages(where=root_folder, include=["taipy", "taipy.core", "taipy.core.*"]),
     include_package_data=True,
-    data_files=[('version', ['version.json'])],
+    data_files=[('version', [version_path])],
     tests_require=test_requirements,
     extras_require=extras_require,
 )

+ 2 - 2
tools/packages/taipy-gui/setup.py

@@ -24,7 +24,7 @@ root_folder = Path(__file__).parent
 
 package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
 
-version_path = os.path.join(root_folder, "taipy/gui/version.json")
+version_path = "taipy/gui/version.json"
 
 setup_requirements = Path("taipy/gui/setup.requirements.txt")
 
@@ -76,7 +76,7 @@ setup(
     install_requires=requirements,
     packages=find_packages(where=root_folder, include=["taipy", "taipy.gui", "taipy.gui.*"]),
     include_package_data=True,
-    data_files=[("version", ["version.json"])],
+    data_files=[("version", [version_path])],
     tests_require=test_requirements,
     extras_require=extras_require,
     cmdclass={"build_py": NPMInstall},

+ 1 - 1
tools/packages/taipy-rest/setup.py

@@ -36,6 +36,6 @@ setup(
     version=version_string,
     packages=find_packages(where=root_folder, include=["taipy", "taipy.rest", "taipy.rest.*"]),
     include_package_data=True,
-    data_files=[('version', ['version.json'])],
+    data_files=[('version', [version_path])],
     install_requires=requirements,
 )

+ 1 - 1
tools/packages/taipy-templates/setup.py

@@ -37,7 +37,7 @@ test_requirements = ["pytest>=3.8"]
 setup(
     packages=find_packages(where=root_folder, include=["taipy"]),
     include_package_data=True,
-    data_files=[('version', ['version.json'])],
+    data_files=[('version', [version_path])],
     test_suite="tests",
     version=version_string,
 )

+ 1 - 1
tools/release/build_package_structure.py

@@ -14,7 +14,7 @@ import shutil
 import sys
 from pathlib import Path
 
-__SKIP = ["LICENSE", "MANIFEST.in", "taipy", "setup.py", "tools"]
+__SKIP = ["LICENSE", "MANIFEST.in", "taipy", "setup.py", "tools", "pyproject.toml"]
 
 
 if __name__ == "__main__":