Parcourir la source

table col width (#2358)

* table col width
resolves #2286

* Fab's comment

Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>

* notSortable => sortable

* Fab's comment

Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>
Fred Lefévère-Laoide il y a 5 mois
Parent
commit
13d97c50e1

+ 2 - 0
frontend/taipy-gui/packaging/taipy-gui.d.ts

@@ -388,6 +388,8 @@ export interface ColumnDesc {
     lov?: string[];
     /** If true the user can enter any value besides the lov values. */
     freeLov?: boolean;
+    /** If false, the column cannot be sorted */
+    sortable?: boolean;
 }
 /**
  * A cell value type.

+ 11 - 0
frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx

@@ -98,6 +98,7 @@ const tableValue = {
     },
 };
 const tableColumns = JSON.stringify({ Entity: { dfid: "Entity" } });
+const tableWidthColumns = JSON.stringify({ Entity: { dfid: "Entity", width: "100px" }, Country: {dfid: "Country"} });
 
 describe("AutoLoadingTable Component", () => {
     it("renders", async () => {
@@ -132,6 +133,16 @@ describe("AutoLoadingTable Component", () => {
         const { queryByTestId } = render(<AutoLoadingTable data={undefined} defaultColumns={tableColumns} active={false} />);
         expect(queryByTestId("ArrowDownwardIcon")).toBeNull();
     });
+    it("hides sort icons when not sortable", async () => {
+        const { queryByTestId } = render(<AutoLoadingTable data={undefined} defaultColumns={tableColumns} sortable={false} />);
+        expect(queryByTestId("ArrowDownwardIcon")).toBeNull();
+    });
+    it("set width if requested", async () => {
+        const { getByText } = render(<AutoLoadingTable data={undefined} defaultColumns={tableWidthColumns} />);
+        const header = getByText("Entity").closest("tr");
+        expect(header?.firstChild).toHaveStyle({"min-width": "100px"});
+        expect(header?.lastChild).toHaveStyle({"width": "100%"});
+    });
     // keep getting undefined Error from jest, it seems to be linked to the setTimeout that makes the code run after the end of the test :-(
     // https://github.com/facebook/jest/issues/12262
     // Looks like the right way to handle this is to use jest fakeTimers and runAllTimers ...

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

@@ -11,63 +11,33 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useState, useEffect, useCallback, useRef, useMemo, CSSProperties, MouseEvent } from "react";
+import React, { CSSProperties, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import AddIcon from "@mui/icons-material/Add";
+import DataSaverOff from "@mui/icons-material/DataSaverOff";
+import DataSaverOn from "@mui/icons-material/DataSaverOn";
+import Download from "@mui/icons-material/Download";
 import Box from "@mui/material/Box";
+import IconButton from "@mui/material/IconButton";
+import Paper from "@mui/material/Paper";
+import Skeleton from "@mui/material/Skeleton";
 import MuiTable from "@mui/material/Table";
 import TableCell, { TableCellProps } from "@mui/material/TableCell";
 import TableContainer from "@mui/material/TableContainer";
 import TableHead from "@mui/material/TableHead";
 import TableRow from "@mui/material/TableRow";
 import TableSortLabel from "@mui/material/TableSortLabel";
-import Paper from "@mui/material/Paper";
+import Tooltip from "@mui/material/Tooltip";
 import { visuallyHidden } from "@mui/utils";
 import AutoSizer from "react-virtualized-auto-sizer";
 import { FixedSizeList, ListOnItemsRenderedProps } from "react-window";
 import InfiniteLoader from "react-window-infinite-loader";
-import Skeleton from "@mui/material/Skeleton";
-import IconButton from "@mui/material/IconButton";
-import Tooltip from "@mui/material/Tooltip";
-import AddIcon from "@mui/icons-material/Add";
-import DataSaverOn from "@mui/icons-material/DataSaverOn";
-import DataSaverOff from "@mui/icons-material/DataSaverOff";
-import Download from "@mui/icons-material/Download";
-import { generateHeaderClassName } from "./tableUtils";
 
 import {
     createRequestInfiniteTableUpdateAction,
     createSendActionNameAction,
     FormatConfig,
 } from "../../context/taipyReducers";
-import {
-    ColumnDesc,
-    FilterDesc,
-    getSortByIndex,
-    Order,
-    TaipyTableProps,
-    baseBoxSx,
-    paperSx,
-    tableSx,
-    RowType,
-    EditableCell,
-    OnCellValidation,
-    RowValue,
-    EDIT_COL,
-    OnRowDeletion,
-    addActionColumn,
-    headBoxSx,
-    getClassName,
-    ROW_CLASS_NAME,
-    iconInRowSx,
-    DEFAULT_SIZE,
-    OnRowSelection,
-    getRowIndex,
-    getTooltip,
-    defaultColumns,
-    OnRowClick,
-    DownloadAction,
-    getFormatFn,
-    getPageKey,
-} from "./tableUtils";
+import { emptyArray } from "../../utils";
 import {
     useClassNames,
     useDispatch,
@@ -78,9 +48,39 @@ import {
     useModule,
 } from "../../utils/hooks";
 import TableFilter from "./TableFilter";
-import { getSuffixedClassNames, getUpdateVar } from "./utils";
-import { emptyArray } from "../../utils";
+import {
+    addActionColumn,
+    baseBoxSx,
+    ColumnDesc,
+    DEFAULT_SIZE,
+    defaultColumns,
+    DownloadAction,
+    EDIT_COL,
+    EditableCell,
+    FilterDesc,
+    generateHeaderClassName,
+    getClassName,
+    getFormatFn,
+    getPageKey,
+    getRowIndex,
+    getSortByIndex,
+    getTooltip,
+    headBoxSx,
+    iconInRowSx,
+    OnCellValidation,
+    OnRowClick,
+    OnRowDeletion,
+    OnRowSelection,
+    Order,
+    paperSx,
+    ROW_CLASS_NAME,
+    RowType,
+    RowValue,
+    tableSx,
+    TaipyTableProps,
+} from "./tableUtils";
 import { getComponentClassName } from "./TaipyStyle";
+import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 
 interface RowData {
     colsOrder: string[];
@@ -203,6 +203,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         compare = false,
         onCompare = "",
         useCheckbox = false,
+        sortable = true,
     } = props;
     const [rows, setRows] = useState<RowType[]>([]);
     const [compRows, setCompRows] = useState<RowType[]>([]);
@@ -253,7 +254,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
     useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
 
     const onSort = useCallback(
-        (e: React.MouseEvent<HTMLElement>) => {
+        (e: MouseEvent<HTMLElement>) => {
             const col = e.currentTarget.getAttribute("data-dfid");
             if (col) {
                 const isAsc = orderBy === col && order === "asc";
@@ -287,82 +288,107 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         e.stopPropagation();
     }, []);
 
-    const [colsOrder, columns, cellClassNames, tooltips, formats, handleNan, filter, partialEditable] = useMemo(() => {
-        let hNan = !!props.nanValue;
-        if (baseColumns) {
-            try {
-                let filter = false;
-                let partialEditable = editable;
-                const newCols: Record<string, ColumnDesc> = {};
-                Object.entries(baseColumns).forEach(([cId, cDesc]) => {
-                    const nDesc = (newCols[cId] = { ...cDesc });
-                    if (typeof nDesc.filter != "boolean") {
-                        nDesc.filter = !!props.filter;
-                    }
-                    filter = filter || nDesc.filter;
-                    if (typeof nDesc.notEditable == "boolean") {
-                        nDesc.notEditable = !editable;
-                    } else {
-                        partialEditable = partialEditable || !nDesc.notEditable;
-                    }
-                    if (nDesc.tooltip === undefined) {
-                        nDesc.tooltip = props.tooltip;
-                    }
-                });
-                addActionColumn(
-                    (active && partialEditable && (onAdd || onDelete) ? 1 : 0) +
-                        (active && filter ? 1 : 0) +
-                        (active && downloadable ? 1 : 0),
-                    newCols
-                );
-                const colsOrder = Object.keys(newCols).sort(getSortByIndex(newCols));
-                const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
-                    if (newCols[col].className) {
-                        pv.classNames = pv.classNames || {};
-                        pv.classNames[newCols[col].dfid] = newCols[col].className as string;
+    const [colsOrder, columns, cellClassNames, tooltips, formats, handleNan, filter, partialEditable, calcWidth] =
+        useMemo(() => {
+            let hNan = !!props.nanValue;
+            if (baseColumns) {
+                try {
+                    let filter = false;
+                    let partialEditable = editable;
+                    const newCols: Record<string, ColumnDesc> = {};
+                    Object.entries(baseColumns).forEach(([cId, cDesc]) => {
+                        const nDesc = (newCols[cId] = { ...cDesc });
+                        if (typeof nDesc.filter != "boolean") {
+                            nDesc.filter = !!props.filter;
+                        }
+                        filter = filter || nDesc.filter;
+                        if (typeof nDesc.notEditable == "boolean") {
+                            nDesc.notEditable = !editable;
+                        } else {
+                            partialEditable = partialEditable || !nDesc.notEditable;
+                        }
+                        if (nDesc.tooltip === undefined) {
+                            nDesc.tooltip = props.tooltip;
+                        }
+                        if (typeof nDesc.sortable != "boolean") {
+                            nDesc.sortable = sortable;
+                        }
+                    });
+                    addActionColumn(
+                        (active && partialEditable && (onAdd || onDelete) ? 1 : 0) +
+                            (active && filter ? 1 : 0) +
+                            (active && downloadable ? 1 : 0),
+                        newCols
+                    );
+                    const colsOrder = Object.keys(newCols).sort(getSortByIndex(newCols));
+                    let nbWidth = 0;
+                    const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
+                        if (newCols[col].className) {
+                            pv.classNames = pv.classNames || {};
+                            pv.classNames[newCols[col].dfid] = newCols[col].className as string;
+                        }
+                        hNan = hNan || !!newCols[col].nanValue;
+                        if (newCols[col].tooltip) {
+                            pv.tooltips = pv.tooltips || {};
+                            pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
+                        }
+                        if (newCols[col].formatFn) {
+                            pv.formats = pv.formats || {};
+                            pv.formats[newCols[col].dfid] = newCols[col].formatFn;
+                        }
+                        if (newCols[col].width !== undefined) {
+                            const cssWidth = getCssSize(newCols[col].width);
+                            if (cssWidth) {
+                                newCols[col].width = cssWidth;
+                                nbWidth++;
+                            }
+                        }
+                        return pv;
+                    }, {});
+                    nbWidth = nbWidth ? colsOrder.length - nbWidth : 0;
+                    if (props.rowClassName) {
+                        styTt.classNames = styTt.classNames || {};
+                        styTt.classNames[ROW_CLASS_NAME] = props.rowClassName;
                     }
-                    hNan = hNan || !!newCols[col].nanValue;
-                    if (newCols[col].tooltip) {
-                        pv.tooltips = pv.tooltips || {};
-                        pv.tooltips[newCols[col].dfid] = newCols[col].tooltip as string;
-                    }
-                    if (newCols[col].formatFn) {
-                        pv.formats = pv.formats || {};
-                        pv.formats[newCols[col].dfid] = newCols[col].formatFn;
-                    }
-                    return pv;
-                }, {});
-                if (props.rowClassName) {
-                    styTt.classNames = styTt.classNames || {};
-                    styTt.classNames[ROW_CLASS_NAME] = props.rowClassName;
+                    return [
+                        colsOrder,
+                        newCols,
+                        styTt.classNames,
+                        styTt.tooltips,
+                        styTt.formats,
+                        hNan,
+                        filter,
+                        partialEditable,
+                        nbWidth > 0 ? `${100 / nbWidth}%` : undefined,
+                    ];
+                } catch (e) {
+                    console.info("ATable.columns: " + ((e as Error).message || e));
                 }
-                return [colsOrder, newCols, styTt.classNames, styTt.tooltips, styTt.formats, hNan, filter, partialEditable];
-            } catch (e) {
-                console.info("ATable.columns: " + ((e as Error).message || e));
             }
-        }
-        return [
-            [],
-            {} as Record<string, ColumnDesc>,
-            {} as Record<string, string>,
-            {} as Record<string, string>,
-            {} as Record<string, string>,
-            hNan,
-            false,
-            false,
-        ];
-    }, [
-        active,
-        editable,
-        onAdd,
-        onDelete,
-        baseColumns,
-        props.rowClassName,
-        props.tooltip,
-        props.nanValue,
-        props.filter,
-        downloadable,
-    ]);
+            return [
+                [],
+                {} as Record<string, ColumnDesc>,
+                {} as Record<string, string>,
+                {} as Record<string, string>,
+                {} as Record<string, string>,
+                hNan,
+                false,
+                false,
+                "",
+            ];
+        }, [
+            active,
+            editable,
+            onAdd,
+            onDelete,
+            baseColumns,
+            props.rowClassName,
+            props.tooltip,
+            props.nanValue,
+            props.filter,
+            downloadable,
+            sortable,
+        ]);
 
     const boxBodySx = useMemo(() => ({ height: height }), [height]);
 
@@ -389,7 +415,18 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             return new Promise<void>((resolve, reject) => {
                 const cols = colsOrder.map((col) => columns[col].dfid).filter((c) => c != EDIT_COL);
                 const afs = appliedFilters.filter((fd) => Object.values(columns).some((cd) => cd.dfid === fd.col));
-                const key = getPageKey(columns, "Infinite", cols, orderBy, order, afs, aggregates, cellClassNames, tooltips, formats);
+                const key = getPageKey(
+                    columns,
+                    "Infinite",
+                    cols,
+                    orderBy,
+                    order,
+                    afs,
+                    aggregates,
+                    cellClassNames,
+                    tooltips,
+                    formats
+                );
                 page.current = {
                     key: key,
                     promises: { ...page.current.promises, [startIndex]: { resolve: resolve, reject: reject } },
@@ -594,7 +631,13 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
     const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]);
 
     return (
-        <Box id={id} sx={boxSx} className={`${className} ${getSuffixedClassNames(className, "-autoloading")} ${getComponentClassName(props.children)}`}>
+        <Box
+            id={id}
+            sx={boxSx}
+            className={`${className} ${getSuffixedClassNames(className, "-autoloading")} ${getComponentClassName(
+                props.children
+            )}`}
+        >
             <Paper sx={paperSx}>
                 <Tooltip title={hover || ""}>
                     <TableContainer>
@@ -605,10 +648,20 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                         <TableCell
                                             key={`head${columns[col].dfid}`}
                                             sortDirection={orderBy === columns[col].dfid && order}
-                                            sx={columns[col].width ? { width: columns[col].width } : {}}
-                                            className={col === "EDIT_COL"
-                                                ? getSuffixedClassNames(className, "-action")
-                                                : getSuffixedClassNames(className, generateHeaderClassName(columns[col].dfid))
+                                            sx={
+                                                columns[col].width
+                                                    ? { minWidth: columns[col].width }
+                                                    : calcWidth
+                                                    ? { width: calcWidth }
+                                                    : undefined
+                                            }
+                                            className={
+                                                col === "EDIT_COL"
+                                                    ? getSuffixedClassNames(className, "-action")
+                                                    : getSuffixedClassNames(
+                                                          className,
+                                                          generateHeaderClassName(columns[col].dfid)
+                                                      )
                                             }
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
@@ -653,8 +706,8 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                                     direction={orderBy === columns[col].dfid ? order : "asc"}
                                                     data-dfid={columns[col].dfid}
                                                     onClick={onSort}
-                                                    disabled={!active}
-                                                    hideSortIcon={!active}
+                                                    disabled={!active || !columns[col].sortable}
+                                                    hideSortIcon={!active || !columns[col].sortable}
                                                 >
                                                     <Box sx={headBoxSx}>
                                                         {columns[col].groupBy ? (

+ 16 - 0
frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx

@@ -94,6 +94,10 @@ const tableColumns = JSON.stringify({
     Entity: { dfid: "Entity" },
     "Daily hospital occupancy": { dfid: "Daily hospital occupancy", type: "int64" },
 });
+const tableWidthColumns = JSON.stringify({
+    Entity: { dfid: "Entity", width: "100px" },
+    "Daily hospital occupancy": { dfid: "Daily hospital occupancy", type: "int64" },
+});
 const changedValue = {
     [valueKey]: {
         data: [
@@ -217,6 +221,18 @@ describe("PaginatedTable Component", () => {
         );
         expect(queryByTestId("ArrowDownwardIcon")).toBeNull();
     });
+    it("Hides sort icons when not sortable", async () => {
+        const { queryByTestId } = render(
+            <PaginatedTable data={undefined} defaultColumns={tableColumns} sortable={false} />
+        );
+        expect(queryByTestId("ArrowDownwardIcon")).toBeNull();
+    });
+    it("set width if requested", async () => {
+        const { getByText } = render(<PaginatedTable data={undefined} defaultColumns={tableWidthColumns} />);
+        const header = getByText("Entity").closest("tr");
+        expect(header?.firstChild).toHaveStyle({"min-width": "100px"});
+        expect(header?.lastChild).toHaveStyle({"width": "100%"});
+    });
     it("dispatch 2 well formed messages at first render", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;

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

@@ -41,7 +41,6 @@ import TableSortLabel from "@mui/material/TableSortLabel";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
 import { visuallyHidden } from "@mui/utils";
-import { generateHeaderClassName } from "./tableUtils";
 
 import { createRequestTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
 import { emptyArray } from "../../utils";
@@ -65,6 +64,7 @@ import {
     EDIT_COL,
     EditableCell,
     FilterDesc,
+    generateHeaderClassName,
     getClassName,
     getFormatFn,
     getPageKey,
@@ -87,7 +87,7 @@ import {
     TaipyPaginatedTableProps,
 } from "./tableUtils";
 import { getComponentClassName } from "./TaipyStyle";
-import { getSuffixedClassNames, getUpdateVar } from "./utils";
+import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 
 const loadingStyle: CSSProperties = { width: "100%", height: "3em", textAlign: "right", verticalAlign: "center" };
 const skeletonSx = { width: "100%", height: "3em" };
@@ -115,6 +115,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         compare = false,
         onCompare = "",
         useCheckbox = false,
+        sortable = true,
     } = props;
     const pageSize = props.pageSize === undefined || props.pageSize < 1 ? 100 : Math.round(props.pageSize);
     const [value, setValue] = useState<Record<string, unknown>>({});
@@ -138,7 +139,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
     const baseColumns = useDynamicJsonProperty(props.columns, props.defaultColumns, defaultColumns);
 
-    const [colsOrder, columns, cellClassNames, tooltips, formats, handleNan, filter, partialEditable, nbWidth] =
+    const [colsOrder, columns, cellClassNames, tooltips, formats, handleNan, filter, partialEditable, calcWidth] =
         useMemo(() => {
             let hNan = !!props.nanValue;
             if (baseColumns) {
@@ -160,6 +161,9 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                         if (nDesc.tooltip === undefined) {
                             nDesc.tooltip = props.tooltip;
                         }
+                        if (typeof nDesc.sortable != "boolean") {
+                            nDesc.sortable = sortable;
+                        }
                     });
                     addActionColumn(
                         (active && partialEditable && (onAdd || onDelete) ? 1 : 0) +
@@ -169,6 +173,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                     );
                     const colsOrder = Object.keys(newCols).sort(getSortByIndex(newCols));
                     let nbWidth = 0;
+                    let widthRate = 0;
                     const functions = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
                         if (newCols[col].className) {
                             pv.classNames = pv.classNames || {};
@@ -184,7 +189,14 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                             pv.formats[newCols[col].dfid] = newCols[col].formatFn;
                         }
                         if (newCols[col].width !== undefined) {
-                            nbWidth++;
+                            const cssWidth = getCssSize(newCols[col].width);
+                            if (cssWidth) {
+                                newCols[col].width = cssWidth;
+                                nbWidth++;
+                                if (cssWidth.endsWith("%")) {
+                                    widthRate += parseInt(cssWidth, 10);
+                                }
+                            }
                         }
                         return pv;
                     }, {});
@@ -202,7 +214,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                         hNan,
                         filter,
                         partialEditable,
-                        nbWidth,
+                        nbWidth > 0 ? `${(100 - widthRate) / nbWidth}%` : undefined
                     ];
                 } catch (e) {
                     console.info("PaginatedTable.columns: ", (e as Error).message || e);
@@ -217,7 +229,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                 hNan,
                 false,
                 false,
-                0,
+                ""
             ];
         }, [
             active,
@@ -230,6 +242,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
             props.nanValue,
             props.filter,
             downloadable,
+            sortable,
         ]);
 
     useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
@@ -524,9 +537,9 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                             sortDirection={orderBy === columns[col].dfid && order}
                                             sx={
                                                 columns[col].width
-                                                    ? { minWidth: columns[col].width }
-                                                    : nbWidth
-                                                    ? { minWidth: `${100 / nbWidth}%` }
+                                                    ? { minWidth:columns[col].width }
+                                                    : calcWidth
+                                                    ? { width: calcWidth }
                                                     : undefined
                                             }
                                             className={col === "EDIT_COL"
@@ -576,8 +589,8 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                                     direction={orderBy === columns[col].dfid ? order : "asc"}
                                                     data-dfid={columns[col].dfid}
                                                     onClick={onSort}
-                                                    disabled={!active}
-                                                    hideSortIcon={!active}
+                                                    disabled={!active || !columns[col].sortable}
+                                                    hideSortIcon={!active || !columns[col].sortable}
                                                 >
                                                     <Box sx={headBoxSx}>
                                                         {columns[col].groupBy ? (

+ 3 - 0
frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

@@ -102,6 +102,8 @@ export interface ColumnDesc {
     lov?: string[];
     /** If true the user can enter any value besides the lov values. */
     freeLov?: boolean;
+    /** If false, the column cannot be sorted */
+    sortable?: boolean;
 }
 
 export const DEFAULT_SIZE = "small";
@@ -156,6 +158,7 @@ export interface TaipyTableProps extends TaipyActiveProps, TaipyMultiSelectProps
     onCompare?: string;
     compare?: boolean;
     useCheckbox?: boolean;
+    sortable?: boolean;
 }
 
 export const DownloadAction = "__Taipy__download_csv";

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

@@ -116,14 +116,14 @@ export const noDisplayStyle = { display: "none" };
 const RE_ONLY_NUMBERS = /^\d+(\.\d*)?$/;
 export const getCssSize = (val: string | number) => {
     if (typeof val === "number") {
-        return "" + val + "px";
+        return `${val}px`;
     } else {
-        val = val.trim();
+        val = `${val}`.trim();
         if (RE_ONLY_NUMBERS.test(val)) {
-            return val + "px";
+            return `${val}px`;
         }
+        return val;
     }
-    return val;
 };
 
 /**

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

@@ -585,6 +585,7 @@ class _Factory:
                 ("size",),
                 ("downloadable", PropertyType.boolean),
                 ("use_checkbox", PropertyType.boolean),
+                ("sortable", PropertyType.boolean, True),
             ]
         )
         ._set_propagate()

+ 7 - 1
taipy/gui/viselements.json

@@ -814,7 +814,7 @@
                     {
                         "name": "width[<i>column_name</i>]",
                         "type": "str",
-                        "doc": "The width of the indicated column, in CSS units."
+                        "doc": "The width of the indicated column, in CSS units (% values are not supported)."
                     },
                     {
                         "name": "selected",
@@ -1076,6 +1076,12 @@
                         "type": "bool",
                         "default_value": "False",
                         "doc": "If True, boolean values are rendered as a simple HTML checkbox."
+                    },
+                    {
+                        "name": "sortable",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If False, the table provides no sorting capability. Individual columns can override this global setting, allowing specific columns to be marked as sortable or non-sortable regardless of value of <i>sortable</i>, by setting the <i>sortable</i> property to True or False accordingly, in the dictionary for that column in the <i>columns</i> property value."
                     }
                 ]
             }