Prechádzať zdrojové kódy

Backports for 4.0.2 (#2336)

* Backport to 4.0 of Taipy GUI issues #2212, #2242, #2281, #2286, #2302, and #2305.
* Upgrade JS bundles versions to 4.0.2
Fabien Lelaquais 5 mesiacov pred
rodič
commit
1a4b844baf

+ 1 - 1
frontend/taipy-gui/base/src/packaging/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "taipy-gui-base",
   "name": "taipy-gui-base",
-  "version": "4.0.1",
+  "version": "4.0.2",
   "private": true,
   "private": true,
   "main": "./taipy-gui-base.js",
   "main": "./taipy-gui-base.js",
   "types": "./taipy-gui-base.d.ts"
   "types": "./taipy-gui-base.d.ts"

+ 1 - 1
frontend/taipy-gui/dom/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "taipy-gui-dom",
   "name": "taipy-gui-dom",
-  "version": "4.0.1",
+  "version": "4.0.2",
   "private": true,
   "private": true,
   "dependencies": {
   "dependencies": {
     "react": "^18.2.0",
     "react": "^18.2.0",

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "taipy-gui",
   "name": "taipy-gui",
-  "version": "4.0.1",
+  "version": "4.0.2",
   "private": true,
   "private": true,
   "dependencies": {
   "dependencies": {
     "@emotion/react": "^11.10.0",
     "@emotion/react": "^11.10.0",

+ 1 - 1
frontend/taipy-gui/packaging/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "taipy-gui",
   "name": "taipy-gui",
-  "version": "4.0.1",
+  "version": "4.0.2",
   "private": true,
   "private": true,
   "main": "./taipy-gui.js",
   "main": "./taipy-gui.js",
   "types": "./taipy-gui.d.ts"
   "types": "./taipy-gui.d.ts"

+ 1 - 1
frontend/taipy-gui/public/stylekit/controls/selector.css

@@ -22,7 +22,7 @@
 **************************************************/
 **************************************************/
 
 
 .taipy-selector {
 .taipy-selector {
-  margin: 4px 0;
+  margin: 0;
 }
 }
 
 
 
 

+ 3 - 0
frontend/taipy-gui/public/stylekit/controls/slider.css

@@ -22,5 +22,8 @@
 **************************************************/
 **************************************************/
 
 
 .taipy-slider {
 .taipy-slider {
+    display: inline-flex;
     max-width: 100%;
     max-width: 100%;
+    min-height: 48px;
+    align-items: center;
 }
 }

+ 28 - 0
frontend/taipy-gui/public/stylekit/controls/status.css

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+/**************************************************************
+
+                   TAIPY STATUS
+
+***************************************************************/
+
+/*************************************************
+              OVERRIDES / NORMALIZATION
+**************************************************/
+
+.taipy-status {
+    display: inline-flex;
+    min-height: 48px;
+    padding-top: 0.5em;
+}

+ 3 - 0
frontend/taipy-gui/public/stylekit/controls/toggle.css

@@ -24,6 +24,9 @@
 .taipy-toggle {
 .taipy-toggle {
     display: inline-flex;
     display: inline-flex;
     z-index: 10;
     z-index: 10;
+    min-height: 48px;
+    align-items: center;
+    gap: 0.3em;
 }
 }
 
 
 .taipy-toggle .MuiToggleButtonGroup-root[aria-label='Theme mode'] {
 .taipy-toggle .MuiToggleButtonGroup-root[aria-label='Theme mode'] {

+ 1 - 0
frontend/taipy-gui/public/stylekit/stylekit.css

@@ -37,6 +37,7 @@
 @import 'controls/number.css';
 @import 'controls/number.css';
 @import 'controls/slider.css';
 @import 'controls/slider.css';
 @import 'controls/selector.css';
 @import 'controls/selector.css';
+@import 'controls/status.css';
 @import 'controls/toggle.css';
 @import 'controls/toggle.css';
 @import 'controls/file_download.css';
 @import 'controls/file_download.css';
 @import 'controls/file_selector.css';
 @import 'controls/file_selector.css';

+ 119 - 117
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -17,6 +17,7 @@ import { useTheme } from "@mui/material";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import Skeleton from "@mui/material/Skeleton";
 import Skeleton from "@mui/material/Skeleton";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
+import merge from "lodash/merge";
 import { nanoid } from "nanoid";
 import { nanoid } from "nanoid";
 import {
 import {
     Config,
     Config,
@@ -156,26 +157,26 @@ const getDecimatorsPayload = (
 ) => {
 ) => {
     return decimators
     return decimators
         ? {
         ? {
-              width: plotDiv?.clientWidth,
-              height: plotDiv?.clientHeight,
-              decimators: decimators.map((d, i) =>
-                  d
-                      ? {
-                            decimator: d,
-                            xAxis: getAxis(traces, i, columns, 0),
-                            yAxis: getAxis(traces, i, columns, 1),
-                            zAxis: getAxis(traces, i, columns, 2),
-                            chartMode: modes[i],
-                        }
-                      : {
-                            xAxis: getAxis(traces, i, columns, 0),
-                            yAxis: getAxis(traces, i, columns, 1),
-                            zAxis: getAxis(traces, i, columns, 2),
-                            chartMode: modes[i],
-                        },
-              ),
-              relayoutData: relayoutData,
-          }
+            width: plotDiv?.clientWidth,
+            height: plotDiv?.clientHeight,
+            decimators: decimators.map((d, i) =>
+                d
+                    ? {
+                        decimator: d,
+                        xAxis: getAxis(traces, i, columns, 0),
+                        yAxis: getAxis(traces, i, columns, 1),
+                        zAxis: getAxis(traces, i, columns, 2),
+                        chartMode: modes[i],
+                    }
+                    : {
+                        xAxis: getAxis(traces, i, columns, 0),
+                        yAxis: getAxis(traces, i, columns, 1),
+                        zAxis: getAxis(traces, i, columns, 2),
+                        chartMode: modes[i],
+                    },
+            ),
+            relayoutData: relayoutData,
+        }
         : undefined;
         : undefined;
 };
 };
 
 
@@ -279,6 +280,11 @@ const updateArrays = (sel: number[][], val: number[], idx: number) => {
     return sel;
     return sel;
 };
 };
 
 
+const getDataKey = (columns?: Record<string, ColumnDesc>, decimators?: string[]): [string[], string] => {
+    const backCols = columns ? Object.values(columns).map((col) => col.dfid) : [];
+    return [backCols, backCols.join("-") + (decimators ? `--${decimators.join("")}` : "")];
+};
+
 const Chart = (props: ChartProp) => {
 const Chart = (props: ChartProp) => {
     const {
     const {
         title = "",
         title = "",
@@ -347,9 +353,8 @@ const Chart = (props: ChartProp) => {
     const config = useDynamicJsonProperty(props.config, props.defaultConfig, defaultConfig);
     const config = useDynamicJsonProperty(props.config, props.defaultConfig, defaultConfig);
 
 
     useEffect(() => {
     useEffect(() => {
-        if (updateVarName && (refresh || !data[dataKey])) {
-            const backCols = Object.values(config.columns).map((col) => col.dfid);
-            const dtKey = backCols.join("-") + (config.decimators ? `--${config.decimators.join("")}` : "");
+        if (updateVarName) {
+            const [backCols, dtKey] = getDataKey(config.columns, config.decimators);
             setDataKey(dtKey);
             setDataKey(dtKey);
             if (refresh || !data[dtKey]) {
             if (refresh || !data[dtKey]) {
                 dispatch(
                 dispatch(
@@ -394,12 +399,10 @@ const Chart = (props: ChartProp) => {
             layout.template = template;
             layout.template = template;
         }
         }
         if (props.figure) {
         if (props.figure) {
-            return {
-                ...(props.figure[0].layout as Partial<Layout>),
-                ...layout,
+            return merge({}, props.figure[0].layout as Partial<Layout>, layout, {
                 title: title || layout.title || (props.figure[0].layout as Partial<Layout>).title,
                 title: title || layout.title || (props.figure[0].layout as Partial<Layout>).title,
                 clickmode: "event+select",
                 clickmode: "event+select",
-            } as Layout;
+            });
         }
         }
         return {
         return {
             ...layout,
             ...layout,
@@ -446,90 +449,94 @@ const Chart = (props: ChartProp) => {
         if (props.figure) {
         if (props.figure) {
             return lastDataPl.current;
             return lastDataPl.current;
         }
         }
-        if (data.__taipy_refresh !== undefined && lastDataPl.current) {
-            return lastDataPl.current;
+        if (data.__taipy_refresh !== undefined) {
+            return lastDataPl.current || [];
+        }
+        const dtKey = getDataKey(config.columns, config.decimators)[1];
+        if (!dataKey.startsWith(dtKey)) {
+            return lastDataPl.current || [];
         }
         }
         const datum = data[dataKey];
         const datum = data[dataKey];
         lastDataPl.current = datum
         lastDataPl.current = datum
             ? config.traces.map((trace, idx) => {
             ? config.traces.map((trace, idx) => {
-                  const ret = {
-                      ...getArrayValue(config.options, idx, {}),
-                      type: config.types[idx],
-                      mode: config.modes[idx],
-                      name:
-                          getArrayValue(config.names, idx) ||
-                          (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined),
-                  } as Record<string, unknown>;
-                  ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) };
-                  if (Object.keys(ret.marker as object).length) {
-                      MARKER_TO_COL.forEach((prop) => {
-                          const val = (ret.marker as Record<string, unknown>)[prop];
-                          if (typeof val === "string") {
-                              const arr = getValueFromCol(datum, val as string);
-                              if (arr.length) {
-                                  (ret.marker as Record<string, unknown>)[prop] = arr;
-                              }
-                          }
-                      });
-                  } else {
-                      delete ret.marker;
-                  }
-                  const xs = getValue(datum, trace, 0) || [];
-                  const ys = getValue(datum, trace, 1) || [];
-                  const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
-                  const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
-                  const baseY = addIndex ? xs : ys;
-                  const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
-                  if (baseX.length) {
-                      if (axisNames.length > 0) {
-                          ret[axisNames[0]] = baseX;
-                      } else {
-                          ret.x = baseX;
-                      }
-                  }
-                  if (baseY.length) {
-                      if (axisNames.length > 1) {
-                          ret[axisNames[1]] = baseY;
-                      } else {
-                          ret.y = baseY;
-                      }
-                  }
-                  const baseZ = getValue(datum, trace, 2, true);
-                  if (baseZ) {
-                      if (axisNames.length > 2) {
-                          ret[axisNames[2]] = baseZ;
-                      } else {
-                          ret.z = baseZ;
-                      }
-                  }
-                  // Hack for treemap charts: create a fallback 'parents' column if needed
-                  // This works ONLY because 'parents' is the third named axis
-                  // (see __CHART_AXIS in gui/utils/chart_config_builder.py)
-                  else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
-                      ret.parents = Array(ret.labels.length).fill("");
-                  }
-                  // Other axis
-                  for (let i = 3; i < axisNames.length; i++) {
-                      ret[axisNames[i]] = getValue(datum, trace, i, true);
-                  }
-                  ret.text = getValue(datum, config.texts, idx, true);
-                  ret.xaxis = config.xaxis[idx];
-                  ret.yaxis = config.yaxis[idx];
-                  ret.hovertext = getValue(datum, config.labels, idx, true);
-                  const selPoints = getArrayValue(selected, idx, []);
-                  if (selPoints?.length) {
-                      ret.selectedpoints = selPoints;
-                  }
-                  ret.orientation = getArrayValue(config.orientations, idx);
-                  ret.line = getArrayValue(config.lines, idx);
-                  ret.textposition = getArrayValue(config.textAnchors, idx);
-                  const selectedMarker = getArrayValue(config.selectedMarkers, idx);
-                  if (selectedMarker) {
-                      ret.selected = { marker: selectedMarker };
-                  }
-                  return ret as Data;
-              })
-            : [];
+                const ret = {
+                    ...getArrayValue(config.options, idx, {}),
+                    type: config.types[idx],
+                    mode: config.modes[idx],
+                    name:
+                        getArrayValue(config.names, idx) ||
+                        (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined),
+                } as Record<string, unknown>;
+                ret.marker = { ...getArrayValue(config.markers, idx, ret.marker || {}) };
+                if (Object.keys(ret.marker as object).length) {
+                    MARKER_TO_COL.forEach((prop) => {
+                        const val = (ret.marker as Record<string, unknown>)[prop];
+                        if (typeof val === "string") {
+                            const arr = getValueFromCol(datum, val as string);
+                            if (arr.length) {
+                                (ret.marker as Record<string, unknown>)[prop] = arr;
+                            }
+                        }
+                    });
+                } else {
+                    delete ret.marker;
+                }
+                const xs = getValue(datum, trace, 0) || [];
+                const ys = getValue(datum, trace, 1) || [];
+                const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
+                const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
+                const baseY = addIndex ? xs : ys;
+                const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
+                if (baseX.length) {
+                    if (axisNames.length > 0) {
+                        ret[axisNames[0]] = baseX;
+                    } else {
+                        ret.x = baseX;
+                    }
+                }
+                if (baseY.length) {
+                    if (axisNames.length > 1) {
+                        ret[axisNames[1]] = baseY;
+                    } else {
+                        ret.y = baseY;
+                    }
+                }
+                const baseZ = getValue(datum, trace, 2, true);
+                if (baseZ) {
+                    if (axisNames.length > 2) {
+                        ret[axisNames[2]] = baseZ;
+                    } else {
+                        ret.z = baseZ;
+                    }
+                }
+                // Hack for treemap charts: create a fallback 'parents' column if needed
+                // This works ONLY because 'parents' is the third named axis
+                // (see __CHART_AXIS in gui/utils/chart_config_builder.py)
+                else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
+                    ret.parents = Array(ret.labels.length).fill("");
+                }
+                // Other axis
+                for (let i = 3; i < axisNames.length; i++) {
+                    ret[axisNames[i]] = getValue(datum, trace, i, true);
+                }
+                ret.text = getValue(datum, config.texts, idx, true);
+                ret.xaxis = config.xaxis[idx];
+                ret.yaxis = config.yaxis[idx];
+                ret.hovertext = getValue(datum, config.labels, idx, true);
+                const selPoints = getArrayValue(selected, idx, []);
+                if (selPoints?.length) {
+                    ret.selectedpoints = selPoints;
+                }
+                ret.orientation = getArrayValue(config.orientations, idx);
+                ret.line = getArrayValue(config.lines, idx);
+                ret.textposition = getArrayValue(config.textAnchors, idx);
+                const selectedMarker = getArrayValue(config.selectedMarkers, idx);
+                if (selectedMarker) {
+                    ret.selected = { marker: selectedMarker };
+                }
+                return ret as Data;
+            })
+            : lastDataPl.current || [];
         return lastDataPl.current;
         return lastDataPl.current;
     }, [props.figure, selected, data, config, dataKey]);
     }, [props.figure, selected, data, config, dataKey]);
 
 
@@ -560,15 +567,10 @@ const Chart = (props: ChartProp) => {
         (eventData: PlotRelayoutEvent) => {
         (eventData: PlotRelayoutEvent) => {
             onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData }));
             onRangeChange && dispatch(createSendActionNameAction(id, module, { action: onRangeChange, ...eventData }));
             if (config.decimators && !config.types.includes("scatter3d")) {
             if (config.decimators && !config.types.includes("scatter3d")) {
-                const backCols = Object.values(config.columns).map((col) => col.dfid);
-                const eventDataKey = Object.entries(eventData)
+                const [backCols, dtKeyBase] = getDataKey(config.columns, config.decimators);
+                const dtKey = `${dtKeyBase}--${Object.entries(eventData)
                     .map(([k, v]) => `${k}=${v}`)
                     .map(([k, v]) => `${k}=${v}`)
-                    .join("-");
-                const dtKey =
-                    backCols.join("-") +
-                    (config.decimators ? `--${config.decimators.join("")}` : "") +
-                    "--" +
-                    eventDataKey;
+                    .join("-")}`;
                 setDataKey(dtKey);
                 setDataKey(dtKey);
                 dispatch(
                 dispatch(
                     createRequestChartUpdateAction(
                     createRequestChartUpdateAction(
@@ -649,8 +651,8 @@ const Chart = (props: ChartProp) => {
                 ? props.figure
                 ? props.figure
                     ? index
                     ? index
                     : data[dataKey].tp_index
                     : data[dataKey].tp_index
-                      ? (data[dataKey].tp_index[index] as number)
-                      : index
+                        ? (data[dataKey].tp_index[index] as number)
+                        : index
                 : 0,
                 : 0,
         [data, dataKey, props.figure],
         [data, dataKey, props.figure],
     );
     );

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

@@ -21,6 +21,7 @@ import React, {
     useEffect,
     useEffect,
     ReactNode,
     ReactNode,
     lazy,
     lazy,
+    UIEvent,
 } from "react";
 } from "react";
 import { SxProps, Theme, darken, lighten } from "@mui/material/styles";
 import { SxProps, Theme, darken, lighten } from "@mui/material/styles";
 import Avatar from "@mui/material/Avatar";
 import Avatar from "@mui/material/Avatar";
@@ -226,6 +227,7 @@ const Chat = (props: ChatProps) => {
     const isAnchorDivVisible = useElementVisible(anchorDivRef);
     const isAnchorDivVisible = useElementVisible(anchorDivRef);
     const [showMessage, setShowMessage] = useState(false);
     const [showMessage, setShowMessage] = useState(false);
     const [anchorPopup, setAnchorPopup] = useState<HTMLDivElement | null>(null);
     const [anchorPopup, setAnchorPopup] = useState<HTMLDivElement | null>(null);
+    const userScrolled = useRef(false);
 
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
@@ -341,7 +343,7 @@ const Chat = (props: ChatProps) => {
     );
     );
 
 
     const showBottom = useCallback(() => {
     const showBottom = useCallback(() => {
-        anchorDivRef.current?.scrollIntoView();
+        anchorDivRef.current?.scrollIntoView && anchorDivRef.current?.scrollIntoView();
         setShowMessage(false);
         setShowMessage(false);
     }, []);
     }, []);
 
 
@@ -367,8 +369,9 @@ const Chat = (props: ChatProps) => {
                 }
                 }
             }
             }
             page.current.key = getChatKey(0, pageSize);
             page.current.key = getChatKey(0, pageSize);
+            !userScrolled.current && showBottom();
         }
         }
-    }, [refresh, pageSize, props.messages]);
+    }, [refresh, pageSize, props.messages, showBottom]);
 
 
     useEffect(() => {
     useEffect(() => {
         if (showMessage && !isAnchorDivVisible) {
         if (showMessage && !isAnchorDivVisible) {
@@ -399,10 +402,14 @@ const Chat = (props: ChatProps) => {
         [loadMoreItems]
         [loadMoreItems]
     );
     );
 
 
+    const handleOnScroll = useCallback((evt: UIEvent) => {
+        userScrolled.current = (evt.target as HTMLDivElement).scrollHeight - (evt.target as HTMLDivElement).offsetHeight - (evt.target as HTMLDivElement).scrollTop > 1;
+    }, []);
+
     return (
     return (
         <Tooltip title={hover || ""}>
         <Tooltip title={hover || ""}>
             <Paper className={className} sx={boxSx} id={id}>
             <Paper className={className} sx={boxSx} id={id}>
-                <Grid container rowSpacing={2} sx={gridSx} ref={scrollDivRef}>
+                <Grid container rowSpacing={2} sx={gridSx} ref={scrollDivRef} onScroll={handleOnScroll}>
                     {rows.length && !rows[0] ? (
                     {rows.length && !rows[0] ? (
                         <Grid className={getSuffixedClassNames(className, "-load")} size={12} sx={noAnchorSx}>
                         <Grid className={getSuffixedClassNames(className, "-load")} size={12} sx={noAnchorSx}>
                             <Box sx={loadMoreSx}>
                             <Box sx={loadMoreSx}>

+ 32 - 26
frontend/taipy-gui/src/components/Taipy/Input.tsx

@@ -51,6 +51,7 @@ const verticalDivStyle: CSSProperties = {
     flexDirection: "column",
     flexDirection: "column",
     gap: 0,
     gap: 0,
 };
 };
+const noPaddingYSx = {py: 0};
 
 
 const Input = (props: TaipyInputProps) => {
 const Input = (props: TaipyInputProps) => {
     const {
     const {
@@ -264,32 +265,36 @@ const Input = (props: TaipyInputProps) => {
         () =>
         () =>
             type == "number"
             type == "number"
                 ? {
                 ? {
-                      htmlInput: {
-                          step: step ? step : 1,
-                          min: min,
-                          max: max,
-                      },
-                      input: {
-                          endAdornment: (
-                              <div style={verticalDivStyle}>
-                                  <IconButton
-                                      aria-label="Increment value"
-                                      size="small"
-                                      onMouseDown={handleUpStepperMouseDown}
-                                  >
-                                      <ArrowDropUpIcon fontSize="inherit" />
-                                  </IconButton>
-                                  <IconButton
-                                      aria-label="Decrement value"
-                                      size="small"
-                                      onMouseDown={handleDownStepperMouseDown}
-                                  >
-                                      <ArrowDropDownIcon fontSize="inherit" />
-                                  </IconButton>
-                              </div>
-                          ),
-                      },
-                  }
+                    htmlInput: {
+                        step: step ? step : 1,
+                        min: min,
+                        max: max,
+                    },
+                    input: {
+                        endAdornment: (
+                            <div style={verticalDivStyle}>
+                                <IconButton
+                                    aria-label="Increment value"
+                                    size="small"
+                                    onMouseDown={handleUpStepperMouseDown}
+                                    disabled={!active}
+                                    sx={noPaddingYSx}
+                                >
+                                    <ArrowDropUpIcon fontSize="inherit" />
+                                </IconButton>
+                                <IconButton
+                                    aria-label="Decrement value"
+                                    size="small"
+                                    onMouseDown={handleDownStepperMouseDown}
+                                    disabled={!active}
+                                    sx={noPaddingYSx}
+                                >
+                                    <ArrowDropDownIcon fontSize="inherit" />
+                                </IconButton>
+                            </div>
+                        ),
+                    },
+                }
                 : type == "password"
                 : type == "password"
                 ? {
                 ? {
                       htmlInput: { autoComplete: "current-password" },
                       htmlInput: { autoComplete: "current-password" },
@@ -308,6 +313,7 @@ const Input = (props: TaipyInputProps) => {
                   }
                   }
                 : undefined,
                 : undefined,
         [
         [
+            active,
             type,
             type,
             step,
             step,
             min,
             min,

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

@@ -503,7 +503,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                             sortDirection={orderBy === columns[col].dfid && order}
                                             sortDirection={orderBy === columns[col].dfid && order}
                                             sx={
                                             sx={
                                                 columns[col].width
                                                 columns[col].width
-                                                    ? { width: columns[col].width }
+                                                    ? { minWidth: columns[col].width }
                                                     : nbWidth
                                                     : nbWidth
                                                     ? { minWidth: `${100 / nbWidth}%` }
                                                     ? { minWidth: `${100 / nbWidth}%` }
                                                     : undefined
                                                     : undefined

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

@@ -173,7 +173,17 @@ const Selector = (props: SelTreeProps) => {
         return sx;
         return sx;
     }, [height]);
     }, [height]);
     const controlSx = useMemo(
     const controlSx = useMemo(
-        () => ({ my: 1, mx: 0, maxWidth: width, display: "flex", "& .MuiFormControl-root": { maxWidth: "unset" } }),
+        () => ({
+            my: 1,
+            mx: 0,
+            maxWidth: width,
+            display: "flex",
+            "& .MuiFormControl-root": {
+                maxWidth: "unset",
+                my: 0,
+                "& .MuiInputBase-root": { minHeight: 48, "& input": { minHeight: "unset" } },
+            },
+        }),
         [width]
         [width]
     );
     );
 
 

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

@@ -37,7 +37,7 @@ describe("StatusList Component", () => {
     it("uses the class", async () => {
     it("uses the class", async () => {
         const { getByText } = render(<StatusList value={statuses} className="taipy-status" />);
         const { getByText } = render(<StatusList value={statuses} className="taipy-status" />);
         const elt = getByText("4 statuses");
         const elt = getByText("4 statuses");
-        expect(elt.parentElement).toHaveClass("taipy-status");
+        expect(elt.parentElement?.parentElement).toHaveClass("taipy-status");
     });
     });
     it("can be opened when more than 1 status", async () => {
     it("can be opened when more than 1 status", async () => {
         const { getByTestId } = render(<StatusList value={statuses} />);
         const { getByTestId } = render(<StatusList value={statuses} />);
@@ -57,8 +57,8 @@ describe("StatusList Component", () => {
         const { getByTestId, getByText } = render(<StatusList value={statuses} />);
         const { getByTestId, getByText } = render(<StatusList value={statuses} />);
         const elt = getByTestId("ArrowDownwardIcon");
         const elt = getByTestId("ArrowDownwardIcon");
         await userEvent.click(elt);
         await userEvent.click(elt);
-        const selt = getByText("info");
-        expect(selt.parentElement?.parentElement?.childElementCount).toBe(4);
+        const infoElt = getByText("info");
+        expect(infoElt.parentElement?.parentElement?.childElementCount).toBe(4);
     });
     });
     it("hide closed statuses", async () => {
     it("hide closed statuses", async () => {
         const { getByTestId, queryAllByTestId } = render(<StatusList value={statuses} />);
         const { getByTestId, queryAllByTestId } = render(<StatusList value={statuses} />);

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

@@ -12,6 +12,7 @@
  */
  */
 
 
 import React, { MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
 import React, { MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
+import Box from "@mui/material/Box";
 import Stack from "@mui/material/Stack";
 import Stack from "@mui/material/Stack";
 import ArrowDownward from "@mui/icons-material/ArrowDownward";
 import ArrowDownward from "@mui/icons-material/ArrowDownward";
 import ArrowUpward from "@mui/icons-material/ArrowUpward";
 import ArrowUpward from "@mui/icons-material/ArrowUpward";
@@ -19,7 +20,7 @@ import Tooltip from "@mui/material/Tooltip";
 import Popover, { PopoverOrigin } from "@mui/material/Popover";
 import Popover, { PopoverOrigin } from "@mui/material/Popover";
 
 
 import Status, { StatusType } from "./Status";
 import Status, { StatusType } from "./Status";
-import { TaipyBaseProps, TaipyHoverProps } from "./utils";
+import { getSuffixedClassNames, TaipyBaseProps, TaipyHoverProps } from "./utils";
 import { useClassNames, useDynamicProperty } from "../../utils/hooks";
 import { useClassNames, useDynamicProperty } from "../../utils/hooks";
 
 
 export const getStatusIntValue = (status: string) => {
 export const getStatusIntValue = (status: string) => {
@@ -157,8 +158,13 @@ const StatusList = (props: StatusListProps) => {
 
 
     return (
     return (
         <Tooltip title={hover || ""}>
         <Tooltip title={hover || ""}>
-            <>
-                <Status id={props.id} value={getGlobalStatus(values)} className={className} {...globalProps} />
+            <Box className={className}>
+                <Status
+                    id={props.id}
+                    value={getGlobalStatus(values)}
+                    className={getSuffixedClassNames(className, "-main")}
+                    {...globalProps}
+                />
                 <Popover open={opened} anchorEl={anchorEl} onClose={onOpen} anchorOrigin={ORIGIN}>
                 <Popover open={opened} anchorEl={anchorEl} onClose={onOpen} anchorOrigin={ORIGIN}>
                     <Stack direction="column" spacing={1}>
                     <Stack direction="column" spacing={1}>
                         {values
                         {values
@@ -170,14 +176,14 @@ const StatusList = (props: StatusListProps) => {
                                         key={getId(props.id, idx)}
                                         key={getId(props.id, idx)}
                                         id={getId(props.id, idx)}
                                         id={getId(props.id, idx)}
                                         value={val}
                                         value={val}
-                                        className={className}
+                                        className={getSuffixedClassNames(className, `-${idx}`)}
                                         {...closeProp}
                                         {...closeProp}
                                     />
                                     />
                                 );
                                 );
                             })}
                             })}
                     </Stack>
                     </Stack>
                 </Popover>
                 </Popover>
-            </>
+            </Box>
         </Tooltip>
         </Tooltip>
     );
     );
 };
 };

+ 1 - 1
frontend/taipy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "taipy-gui-core",
   "name": "taipy-gui-core",
-  "version": "4.0.1",
+  "version": "4.0.2",
   "private": true,
   "private": true,
   "devDependencies": {
   "devDependencies": {
     "@types/react": "^18.0.15",
     "@types/react": "^18.0.15",

+ 3 - 2
taipy/gui/builder/_utils.py

@@ -39,10 +39,11 @@ class _TransformVarToValue(ast.NodeTransformer):
         if var_parts[0] in self.non_vars:
         if var_parts[0] in self.non_vars:
             return node
             return node
         value = _get_value_in_frame(self.frame, var_parts[0])
         value = _get_value_in_frame(self.frame, var_parts[0])
-        if callable(value):
-            return node
         if len(var_parts) > 1:
         if len(var_parts) > 1:
             value = attrgetter(var_parts[1])(value)
             value = attrgetter(var_parts[1])(value)
+        if not isinstance(value, (str, int, float, bool, list, tuple)):
+            # transform into constants only what can be (ie not callable or generator for example)
+            return node
         return ast.Constant(value=value, kind=None)
         return ast.Constant(value=value, kind=None)
 
 
 
 

+ 2 - 0
taipy/gui/utils/_evaluator.py

@@ -255,6 +255,8 @@ class _Evaluator:
             # evaluate expressions
             # evaluate expressions
             ctx: t.Dict[str, t.Any] = {}
             ctx: t.Dict[str, t.Any] = {}
             ctx.update(self.__global_ctx)
             ctx.update(self.__global_ctx)
+            if lambda_expr:
+                ctx.update(gui._get_locals_bind())
             # entries in var_val are not always seen (NameError) when passed as locals
             # entries in var_val are not always seen (NameError) when passed as locals
             ctx.update(var_val)
             ctx.update(var_val)
             with gui._get_authorization():
             with gui._get_authorization():

+ 25 - 0
tests/gui/gui_specific/test_evaluator.py

@@ -0,0 +1,25 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+
+from taipy.gui import Gui
+from taipy.gui.utils._evaluator import _Evaluator
+
+
+def _identity(x):
+    return x
+
+def test_evaluate_expr_lambda_from_element(gui: Gui, test_client, helpers):
+    gui._Gui__evaluator = _Evaluator({}, []) # type: ignore[attr-defined]
+    gui._Gui__locals_context.add("a_module", {"identity": _identity}) # type: ignore[attr-defined]
+    with gui._set_locals_context("a_module"):
+        evaluated_expr: str = gui._evaluate_expr("lambda x: identity(x)", lambda_expr=True)
+        assert evaluated_expr.startswith("__lambda_")
+        assert evaluated_expr.endswith("_TPMDL_0")