diff --git a/frontend/taipy-gui/packaging/taipy-gui.d.ts b/frontend/taipy-gui/packaging/taipy-gui.d.ts index 1521ae5291..1df9b82b44 100644 --- a/frontend/taipy-gui/packaging/taipy-gui.d.ts +++ b/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. diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx index 36b39da5a6..834496e399 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx +++ b/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(); expect(queryByTestId("ArrowDownwardIcon")).toBeNull(); }); + it("hides sort icons when not sortable", async () => { + const { queryByTestId } = render(); + expect(queryByTestId("ArrowDownwardIcon")).toBeNull(); + }); + it("set width if requested", async () => { + const { getByText } = render(); + 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 ... diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx index ccd704d4e1..f1bb0e48f1 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx +++ b/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([]); const [compRows, setCompRows] = useState([]); @@ -251,7 +252,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); const onSort = useCallback( - (e: React.MouseEvent) => { + (e: MouseEvent) => { 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 = {}; - 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 = {}; + 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>>((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>>((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, - {} as Record, - {} as Record, - {} as Record, - hNan, - false, - false, - ]; - }, [ - active, - editable, - onAdd, - onDelete, - baseColumns, - props.rowClassName, - props.tooltip, - props.nanValue, - props.filter, - downloadable, - ]); + return [ + [], + {} as Record, + {} as Record, + {} as Record, + {} as Record, + 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((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) => { {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} > {columns[col].groupBy ? ( diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx index c2971bfecf..765e038cad 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx +++ b/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( + + ); + expect(queryByTestId("ArrowDownwardIcon")).toBeNull(); + }); + it("set width if requested", async () => { + const { getByText } = render(); + 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; diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx index 8d36c1751a..fa882a5ef2 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx +++ b/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>({}); @@ -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 = {}; - 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>>((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 = {}; + 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>>((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, - {} as Record, - {} as Record, - {} as Record, - hNan, - false, - false, - 0, - ]; - }, [ - active, - editable, - onAdd, - onDelete, - baseColumns, - props.rowClassName, - props.tooltip, - props.nanValue, - props.filter, - downloadable, - ]); + return [ + [] as string[], + {} as Record, + {} as Record, + {} as Record, + {} as Record, + 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} > {columns[col].groupBy ? ( diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx index cde96f8db3..8c4795417d 100644 --- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx +++ b/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"; diff --git a/frontend/taipy-gui/src/components/Taipy/utils.ts b/frontend/taipy-gui/src/components/Taipy/utils.ts index a769012411..203db6dcc6 100644 --- a/frontend/taipy-gui/src/components/Taipy/utils.ts +++ b/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) => diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 2ad05c4428..c7be2941c1 100644 --- a/taipy/gui/_renderers/factory.py +++ b/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() diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 3cf59f44c2..66bb83c0a3 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -739,7 +739,7 @@ { "name": "width[column_name]", "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 sortable, by setting the sortable property to True or False accordingly, in the dictionary for that column in the columns property value." } ] }