Skip to content

Commit

Permalink
backport: table col width
Browse files Browse the repository at this point in the history
resolves #2358
  • Loading branch information
Fred Lefévère-Laoide authored and Fred Lefévère-Laoide committed Dec 20, 2024
1 parent 996f524 commit 584d5bd
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 243 deletions.
2 changes: 2 additions & 0 deletions frontend/taipy-gui/packaging/taipy-gui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 ...
Expand Down
281 changes: 162 additions & 119 deletions frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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[];
Expand Down Expand Up @@ -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[]>([]);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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]);

Expand All @@ -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 } },
Expand Down Expand Up @@ -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 ? (
[
Expand Down Expand Up @@ -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 ? (
Expand Down
Loading

0 comments on commit 584d5bd

Please sign in to comment.