Browse Source

backport: table col width (#2360)

resolves #2358

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 5 months ago
parent
commit
b679f9c04a

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

@@ -370,6 +370,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 ...

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

@@ -11,62 +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 {
     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,
@@ -77,8 +48,37 @@ 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,
+    getClassName,
+    getFormatFn,
+    getPageKey,
+    getRowIndex,
+    getSortByIndex,
+    getTooltip,
+    headBoxSx,
+    iconInRowSx,
+    OnCellValidation,
+    OnRowClick,
+    OnRowDeletion,
+    OnRowSelection,
+    Order,
+    paperSx,
+    ROW_CLASS_NAME,
+    RowType,
+    RowValue,
+    tableSx,
+    TaipyTableProps,
+} from "./tableUtils";
+import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
 
 interface RowData {
     colsOrder: string[];
@@ -201,6 +201,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         compare = false,
         onCompare = "",
         useCheckbox = false,
+        sortable = true,
     } = props;
     const [rows, setRows] = useState<RowType[]>([]);
     const [compRows, setCompRows] = useState<RowType[]>([]);
@@ -251,7 +252,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";
@@ -285,82 +286,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;
+    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;
                     }
-                });
-                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;
-                    }
-                    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]);
 
@@ -387,7 +413,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 } },
@@ -603,7 +640,13 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
                                         <TableCell
                                             key={`head${columns[col].dfid}`}
                                             sortDirection={orderBy === columns[col].dfid && order}
-                                            sx={columns[col].width ? { width: columns[col].width } : undefined}
+                                            sx={
+                                                columns[col].width
+                                                    ? { minWidth: columns[col].width }
+                                                    : calcWidth
+                                                    ? { width: calcWidth }
+                                                    : undefined
+                                            }
                                         >
                                             {columns[col].dfid === EDIT_COL ? (
                                                 [
@@ -647,8 +690,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;

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

@@ -42,49 +42,49 @@ import DataSaverOff from "@mui/icons-material/DataSaverOff";
 import Download from "@mui/icons-material/Download";
 
 import { createRequestTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
+import { emptyArray } from "../../utils";
+import {
+    useClassNames,
+    useDispatch,
+    useDispatchRequestUpdateOnFirstRender,
+    useDynamicJsonProperty,
+    useDynamicProperty,
+    useFormatConfig,
+    useModule,
+} from "../../utils/hooks";
+import TableFilter from "./TableFilter";
 import {
     addActionColumn,
     baseBoxSx,
+    ColumnDesc,
+    DEFAULT_SIZE,
     defaultColumns,
-    EditableCell,
+    DownloadAction,
     EDIT_COL,
+    EditableCell,
+    FilterDesc,
     getClassName,
+    getFormatFn,
+    getPageKey,
+    getRowIndex,
     getSortByIndex,
+    getTooltip,
     headBoxSx,
-    ROW_CLASS_NAME,
+    iconInRowSx,
     OnCellValidation,
+    OnRowClick,
     OnRowDeletion,
+    OnRowSelection,
     Order,
     PageSizeOptionsType,
     paperSx,
+    ROW_CLASS_NAME,
     RowType,
     RowValue,
     tableSx,
     TaipyPaginatedTableProps,
-    ColumnDesc,
-    iconInRowSx,
-    DEFAULT_SIZE,
-    OnRowSelection,
-    getRowIndex,
-    getTooltip,
-    OnRowClick,
-    DownloadAction,
-    getFormatFn,
-    getPageKey,
-    FilterDesc,
 } from "./tableUtils";
-import {
-    useClassNames,
-    useDispatch,
-    useDispatchRequestUpdateOnFirstRender,
-    useDynamicJsonProperty,
-    useDynamicProperty,
-    useFormatConfig,
-    useModule,
-} from "../../utils/hooks";
-import TableFilter from "./TableFilter";
-import { getSuffixedClassNames, getUpdateVar } from "./utils";
-import { emptyArray } 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" };
@@ -112,6 +112,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>>({});
@@ -135,98 +136,111 @@ 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] = 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") {
-                        partialEditable = partialEditable || !nDesc.notEditable;
-                    } else {
-                        nDesc.notEditable = !editable;
-                    }
-                    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));
-                let nbWidth = 0;
-                const functions = 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;
+    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") {
+                            partialEditable = partialEditable || !nDesc.notEditable;
+                        } else {
+                            nDesc.notEditable = !editable;
+                        }
+                        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;
+                    let widthRate = 0;
+                    const functions = 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;
+                        }
+                        hNan = hNan || !!newCols[col].nanValue;
+                        if (newCols[col].tooltip) {
+                            pv.tooltips = pv.tooltips || {};
+                            pv.tooltips[newCols[col].dfid] = newCols[col].tooltip;
+                        }
+                        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++;
+                                if (cssWidth.endsWith("%")) {
+                                    widthRate += parseInt(cssWidth, 10);
+                                }
+                            }
+                        }
+                        return pv;
+                    }, {});
+                    nbWidth = nbWidth ? colsOrder.length - nbWidth : 0;
+                    if (props.rowClassName) {
+                        functions.classNames = functions.classNames || {};
+                        functions.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;
-                    }
-                    if (newCols[col].formatFn) {
-                        pv.formats = pv.formats || {};
-                        pv.formats[newCols[col].dfid] = newCols[col].formatFn;
-                    }
-                    if (newCols[col].width !== undefined) {
-                        nbWidth++;
-                    }
-                    return pv;
-                }, {});
-                nbWidth = nbWidth ? colsOrder.length - nbWidth : 0;
-                if (props.rowClassName) {
-                    functions.classNames = functions.classNames || {};
-                    functions.classNames[ROW_CLASS_NAME] = props.rowClassName;
+                    return [
+                        colsOrder,
+                        newCols,
+                        functions.classNames,
+                        functions.tooltips,
+                        functions.formats,
+                        hNan,
+                        filter,
+                        partialEditable,
+                        nbWidth > 0 ? `${(100 - widthRate) / nbWidth}%` : undefined
+                    ];
+                } catch (e) {
+                    console.info("PaginatedTable.columns: ", (e as Error).message || e);
                 }
-                return [
-                    colsOrder,
-                    newCols,
-                    functions.classNames,
-                    functions.tooltips,
-                    functions.formats,
-                    hNan,
-                    filter,
-                    partialEditable,
-                    nbWidth,
-                ];
-            } catch (e) {
-                console.info("PaginatedTable.columns: ", (e as Error).message || e);
             }
-        }
-        return [
-            [] as string[],
-            {} as Record<string, ColumnDesc>,
-            {} as Record<string, string>,
-            {} as Record<string, string>,
-            {} as Record<string, string>,
-            hNan,
-            false,
-            false,
-            0,
-        ];
-    }, [
-        active,
-        editable,
-        onAdd,
-        onDelete,
-        baseColumns,
-        props.rowClassName,
-        props.tooltip,
-        props.nanValue,
-        props.filter,
-        downloadable,
-    ]);
+            return [
+                [] as string[],
+                {} 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,
+        ]);
 
     useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
 
@@ -503,9 +517,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
                                             }
                                         >
@@ -551,8 +565,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

@@ -86,6 +86,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";
@@ -140,6 +142,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

@@ -113,14 +113,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;
 };
 
 export const getSuffixedClassNames = (names: string | undefined, suffix: string) =>

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

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

+ 7 - 1
taipy/gui/viselements.json

@@ -739,7 +739,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",
@@ -1001,6 +1001,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."
                     }
                 ]
             }