123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- /*
- * Copyright 2021-2024 Avaiga Private Limited
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- */
- import React, {
- ChangeEvent,
- 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 Table from "@mui/material/Table";
- import TableBody from "@mui/material/TableBody";
- import TableCell from "@mui/material/TableCell";
- import TableContainer from "@mui/material/TableContainer";
- import TableHead from "@mui/material/TableHead";
- import TablePagination from "@mui/material/TablePagination";
- import TableRow from "@mui/material/TableRow";
- 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";
- import {
- useClassNames,
- useDispatch,
- useDispatchRequestUpdateOnFirstRender,
- useDynamicJsonProperty,
- useDynamicProperty,
- useFormatConfig,
- useModule,
- } from "../../utils/hooks";
- import TableFilter from "./TableFilter";
- 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,
- PageSizeOptionsType,
- paperSx,
- ROW_CLASS_NAME,
- RowType,
- RowValue,
- tableSx,
- TaipyPaginatedTableProps,
- } from "./tableUtils";
- import { getComponentClassName } from "./TaipyStyle";
- import { getSuffixedClassNames, getUpdateVar } from "./utils";
- const loadingStyle: CSSProperties = { width: "100%", height: "3em", textAlign: "right", verticalAlign: "center" };
- const skeletonSx = { width: "100%", height: "3em" };
- const rowsPerPageOptions: PageSizeOptionsType = [10, 50, 100, 500];
- const PaginatedTable = (props: TaipyPaginatedTableProps) => {
- const {
- id,
- updateVarName,
- pageSizeOptions,
- allowAllRows = false,
- showAll = false,
- height,
- selected = emptyArray,
- updateVars,
- onEdit = "",
- onDelete = "",
- onAdd = "",
- onAction = "",
- width = "100%",
- size = DEFAULT_SIZE,
- userData,
- downloadable = false,
- compare = false,
- onCompare = "",
- useCheckbox = false,
- } = props;
- const pageSize = props.pageSize === undefined || props.pageSize < 1 ? 100 : Math.round(props.pageSize);
- const [value, setValue] = useState<Record<string, unknown>>({});
- const [startIndex, setStartIndex] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(pageSize);
- const [order, setOrder] = useState<Order>("asc");
- const [orderBy, setOrderBy] = useState("");
- const [loading, setLoading] = useState(true);
- const [aggregates, setAggregates] = useState<string[]>([]);
- const [appliedFilters, setAppliedFilters] = useState<FilterDesc[]>([]);
- const dispatch = useDispatch();
- const pageKey = useRef("no-page");
- const selectedRowRef = useRef<HTMLTableRowElement | null>(null);
- const formatConfig = useFormatConfig();
- const module = useModule();
- const refresh = props.data?.__taipy_refresh !== undefined;
- const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
- const active = useDynamicProperty(props.active, props.defaultActive, true);
- const editable = useDynamicProperty(props.editable, props.defaultEditable, false);
- 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;
- }
- 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,
- ];
- } 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,
- ]);
- useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
- /*
- TODO: If the 'selected' value is a negative number, it will lead to unexpected pagination behavior.
- For instance, if 'selected' is -1, the pagination will display from -99 to 0 and no data will be selected.
- Need to fix this issue.
- */
- useEffect(() => {
- if (selected.length) {
- if (selected[0] < startIndex || selected[0] > startIndex + rowsPerPage) {
- setLoading(true);
- setStartIndex(rowsPerPage * Math.floor(selected[0] / rowsPerPage));
- }
- }
- }, [selected, startIndex, rowsPerPage]);
- useEffect(() => {
- if (!refresh && props.data && props.data[pageKey.current] !== undefined) {
- setValue(props.data[pageKey.current]);
- setLoading(false);
- }
- }, [refresh, props.data]);
- useEffect(() => {
- const endIndex = showAll ? -1 : startIndex + rowsPerPage - 1;
- 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));
- pageKey.current = getPageKey(
- columns,
- `${startIndex}-${endIndex}`,
- cols,
- orderBy,
- order,
- afs,
- aggregates,
- cellClassNames,
- tooltips,
- formats
- );
- if (refresh || !props.data || props.data[pageKey.current] === undefined) {
- setLoading(true);
- const applies = aggregates.length
- ? colsOrder.reduce<Record<string, unknown>>((pv, col) => {
- if (columns[col].apply) {
- pv[columns[col].dfid] = columns[col].apply;
- }
- return pv;
- }, {})
- : undefined;
- dispatch(
- createRequestTableUpdateAction(
- updateVarName,
- id,
- module,
- cols,
- pageKey.current,
- startIndex,
- endIndex,
- orderBy,
- order,
- aggregates,
- applies,
- cellClassNames,
- tooltips,
- formats,
- handleNan,
- afs,
- compare ? onCompare : undefined,
- updateVars && getUpdateVar(updateVars, "comparedatas"),
- typeof userData == "object"
- ? (userData as Record<string, Record<string, unknown>>).context
- : undefined
- )
- );
- } else {
- setValue(props.data[pageKey.current]);
- setLoading(false);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- refresh,
- startIndex,
- aggregates,
- colsOrder,
- columns,
- showAll,
- cellClassNames,
- tooltips,
- formats,
- rowsPerPage,
- order,
- orderBy,
- updateVarName,
- updateVars,
- id,
- handleNan,
- appliedFilters,
- dispatch,
- module,
- compare,
- onCompare,
- userData,
- ]);
- const onSort = useCallback(
- (e: MouseEvent<HTMLElement>) => {
- const col = e.currentTarget.getAttribute("data-dfid");
- if (col) {
- const isAsc = orderBy === col && order === "asc";
- setOrder(isAsc ? "desc" : "asc");
- setOrderBy(col);
- }
- },
- [orderBy, order]
- );
- const handleChangePage = useCallback(
- (event: unknown, newPage: number) => {
- setStartIndex(newPage * rowsPerPage);
- },
- [rowsPerPage]
- );
- const handleChangeRowsPerPage = useCallback((event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
- setLoading(true);
- setRowsPerPage(parseInt(event.target.value, 10));
- setStartIndex(0);
- }, []);
- const onAggregate = useCallback((e: MouseEvent<HTMLElement>) => {
- const groupBy = e.currentTarget.getAttribute("data-dfid");
- if (groupBy) {
- setAggregates((ags) => {
- const nags = ags.filter((ag) => ag !== groupBy);
- if (ags.length == nags.length) {
- nags.push(groupBy);
- }
- return nags;
- });
- }
- e.stopPropagation();
- }, []);
- const onAddRowClick = useCallback(
- () =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onAdd,
- index: startIndex,
- user_data: userData,
- })
- ),
- [startIndex, dispatch, updateVarName, onAdd, module, userData]
- );
- const onDownload = useCallback(
- () =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: DownloadAction,
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, module, userData]
- );
- const tableContainerSx = useMemo(() => ({ maxHeight: height }), [height]);
- const pso = useMemo(() => {
- let psOptions = rowsPerPageOptions;
- if (pageSizeOptions) {
- try {
- psOptions = JSON.parse(pageSizeOptions);
- } catch (e) {
- console.log("PaginatedTable pageSizeOptions is wrong ", pageSizeOptions, e);
- }
- }
- if (
- pageSize > 0 &&
- !psOptions.some((ps) =>
- typeof ps === "number" ? ps === pageSize : typeof ps.value === "number" ? ps.value === pageSize : false
- )
- ) {
- psOptions.push({ value: pageSize, label: "" + pageSize });
- }
- if (allowAllRows) {
- psOptions.push({ value: -1, label: "All" });
- }
- psOptions.sort((a, b) => (typeof a === "number" ? a : a.value) - (typeof b === "number" ? b : b.value));
- return psOptions;
- }, [pageSizeOptions, allowAllRows, pageSize]);
- const { rows, rowCount, filteredCount, compRows } = useMemo(() => {
- const ret = { rows: undefined, rowCount: 0, filteredCount: 0, compRows: [] } as {
- rows?: RowType[];
- rowCount: number;
- filteredCount: number;
- compRows: RowType[];
- };
- if (value) {
- if (value.data) {
- ret.rows = value.data as RowType[];
- }
- if (value.rowcount) {
- ret.rowCount = value.rowcount as unknown as number;
- if (value.fullrowcount && value.rowcount != value.fullrowcount) {
- ret.filteredCount = (value.fullrowcount as unknown as number) - ret.rowCount;
- }
- }
- if (value.comp) {
- ret.compRows = value.comp as RowType[];
- }
- }
- return ret;
- }, [value]);
- const onCellValidation: OnCellValidation = useCallback(
- (value: RowValue, rowIndex: number, colName: string, userValue: string, tz?: string) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onEdit,
- value: value,
- index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex) : startIndex,
- col: colName,
- user_value: userValue,
- tz: tz,
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onEdit, rows, startIndex, module, userData]
- );
- const onRowDeletion: OnRowDeletion = useCallback(
- (rowIndex: number) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onDelete,
- index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex): startIndex,
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onDelete, rows, startIndex, module, userData]
- );
- const onRowSelection: OnRowSelection = useCallback(
- (rowIndex: number, colName?: string, value?: string) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onAction,
- index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex): startIndex,
- col: colName === undefined ? null : colName,
- value,
- reason: value === undefined ? "click" : "button",
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onAction, rows, startIndex, module, userData]
- );
- const onRowClick: OnRowClick = useCallback(
- (e: MouseEvent<HTMLTableRowElement>) => {
- const { index } = e.currentTarget.dataset || {};
- const rowIndex = index === undefined ? NaN : Number(index);
- if (!isNaN(rowIndex)) {
- onRowSelection(rowIndex);
- }
- },
- [onRowSelection]
- );
- const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]);
- return (
- <Box
- id={id}
- sx={boxSx}
- className={`${className} ${getSuffixedClassNames(className, "-paginated")} ${getComponentClassName(
- props.children
- )}`}
- >
- <Paper sx={paperSx}>
- <Tooltip title={hover || ""}>
- <TableContainer sx={tableContainerSx}>
- <Table sx={tableSx} aria-labelledby="tableTitle" size={size} stickyHeader={true}>
- <TableHead>
- <TableRow>
- {colsOrder.map((col) => (
- <TableCell
- key={`head${columns[col].dfid}`}
- sortDirection={orderBy === columns[col].dfid && order}
- sx={
- columns[col].width
- ? { width: columns[col].width }
- : nbWidth
- ? { minWidth: `${100 / nbWidth}%` }
- : undefined
- }
- className={col === "EDIT_COL"
- ? getSuffixedClassNames(className, "-action")
- : getSuffixedClassNames(className, generateHeaderClassName(columns[col].dfid))
- }
- >
- {columns[col].dfid === EDIT_COL ? (
- [
- active && (editable || partialEditable) && onAdd ? (
- <Tooltip title="Add a row" key="addARow">
- <IconButton
- onClick={onAddRowClick}
- size="small"
- sx={iconInRowSx}
- >
- <AddIcon fontSize="inherit" />
- </IconButton>
- </Tooltip>
- ) : null,
- active && filter ? (
- <TableFilter
- key="filter"
- columns={columns}
- colsOrder={colsOrder}
- onValidate={setAppliedFilters}
- appliedFilters={appliedFilters}
- className={className}
- filteredCount={filteredCount}
- />
- ) : null,
- active && downloadable ? (
- <Tooltip title="Download as CSV" key="downloadCsv">
- <IconButton
- onClick={onDownload}
- size="small"
- sx={iconInRowSx}
- >
- <Download fontSize="inherit" />
- </IconButton>
- </Tooltip>
- ) : null,
- ]
- ) : (
- <TableSortLabel
- active={orderBy === columns[col].dfid}
- direction={orderBy === columns[col].dfid ? order : "asc"}
- data-dfid={columns[col].dfid}
- onClick={onSort}
- disabled={!active}
- hideSortIcon={!active}
- >
- <Box sx={headBoxSx}>
- {columns[col].groupBy ? (
- <IconButton
- onClick={onAggregate}
- size="small"
- title="aggregate"
- data-dfid={columns[col].dfid}
- disabled={!active}
- sx={iconInRowSx}
- >
- {aggregates.includes(columns[col].dfid) ? (
- <DataSaverOff fontSize="inherit" />
- ) : (
- <DataSaverOn fontSize="inherit" />
- )}
- </IconButton>
- ) : null}
- {columns[col].title === undefined
- ? columns[col].dfid
- : columns[col].title}
- </Box>
- {orderBy === columns[col].dfid ? (
- <Box component="span" sx={visuallyHidden}>
- {order === "desc"
- ? "sorted descending"
- : "sorted ascending"}
- </Box>
- ) : null}
- </TableSortLabel>
- )}
- </TableCell>
- ))}
- </TableRow>
- </TableHead>
- <TableBody>
- {rows?.map((row, index) => {
- const sel = selected.indexOf(index + startIndex);
- if (sel == 0) {
- Promise.resolve().then(
- () =>
- selectedRowRef.current?.scrollIntoView &&
- selectedRowRef.current.scrollIntoView({ block: "center" })
- );
- }
- return (
- <TableRow
- hover
- tabIndex={-1}
- key={`row${index}`}
- selected={sel > -1}
- ref={sel == 0 ? selectedRowRef : undefined}
- className={getClassName(row, props.rowClassName)}
- data-index={index}
- onClick={active && onAction ? onRowClick : undefined}
- >
- {colsOrder.map((col) => (
- <EditableCell
- key={`cell${index}${columns[col].dfid}`}
- className={getClassName(row, columns[col].className, col)}
- tableClassName={className}
- colDesc={columns[col]}
- value={row[col]}
- formattedVal={getFormatFn(row, columns[col].formatFn, col)}
- formatConfig={formatConfig}
- rowIndex={index}
- onValidation={
- active && !columns[col].notEditable && onEdit
- ? onCellValidation
- : undefined
- }
- onDeletion={
- active && (editable || partialEditable) && onDelete
- ? onRowDeletion
- : undefined
- }
- onSelection={active && onAction ? onRowSelection : undefined}
- nanValue={columns[col].nanValue || props.nanValue}
- tooltip={getTooltip(row, columns[col].tooltip, col)}
- comp={compRows && compRows[index] && compRows[index][col]}
- useCheckbox={useCheckbox}
- />
- ))}
- </TableRow>
- );
- })}
- {!rows &&
- loading &&
- Array.from(Array(30).keys(), (v, idx) => (
- <TableRow hover key={"rowSkel" + idx}>
- {colsOrder.map((col, cIdx) => (
- <TableCell key={"skel" + cIdx}>
- <Skeleton sx={skeletonSx} />
- </TableCell>
- ))}
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </TableContainer>
- </Tooltip>
- {!showAll &&
- (loading ? (
- <Skeleton sx={loadingStyle}>
- <Typography>Loading...</Typography>
- </Skeleton>
- ) : (
- <TablePagination
- component="div"
- count={rowCount}
- page={startIndex / rowsPerPage}
- rowsPerPage={rowsPerPage}
- showFirstButton={true}
- showLastButton={true}
- rowsPerPageOptions={pso}
- onPageChange={handleChangePage}
- onRowsPerPageChange={handleChangeRowsPerPage}
- />
- ))}
- </Paper>
- {props.children}
- </Box>
- );
- };
- export default PaginatedTable;
|