diff --git a/frontend/taipy-gui/packaging/taipy-gui.d.ts b/frontend/taipy-gui/packaging/taipy-gui.d.ts
index bf07e4786..ab7025678 100644
--- a/frontend/taipy-gui/packaging/taipy-gui.d.ts
+++ b/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.
diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx
index 36b39da5a..834496e39 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 fabc2cafe..0b73046d2 100644
--- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
+++ b/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([]);
const [compRows, setCompRows] = useState([]);
@@ -253,7 +254,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";
@@ -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 = {};
- 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>>((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 = {};
+ 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;
}
- 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]);
@@ -389,7 +415,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 } },
@@ -594,7 +631,13 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]);
return (
-
+
@@ -605,10 +648,20 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
{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}
>
{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 c2971bfec..765e038ca 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 c5d28f2ec..f0acba85e 100644
--- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
+++ b/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>({});
@@ -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>>((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}
>
{columns[col].groupBy ? (
diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx
index 3ef9c8845..2b3f295ab 100644
--- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx
+++ b/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";
diff --git a/frontend/taipy-gui/src/components/Taipy/utils.ts b/frontend/taipy-gui/src/components/Taipy/utils.ts
index a714cbbea..1e82d2d03 100644
--- a/frontend/taipy-gui/src/components/Taipy/utils.ts
+++ b/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;
};
/**
diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py
index 7a45cdb90..f6126944c 100644
--- a/taipy/gui/_renderers/factory.py
+++ b/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()
diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json
index 452ec035f..4cf152bd9 100644
--- a/taipy/gui/viselements.json
+++ b/taipy/gui/viselements.json
@@ -814,7 +814,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",
@@ -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 sortable, by setting the sortable property to True or False accordingly, in the dictionary for that column in the columns property value."
}
]
}