123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 |
- /*
- * 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, { useState, useEffect, useCallback, useRef, useMemo, CSSProperties, MouseEvent } from "react";
- import Box from "@mui/material/Box";
- 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 { 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,
- getsortByIndex,
- Order,
- TaipyTableProps,
- baseBoxSx,
- paperSx,
- tableSx,
- RowType,
- EditableCell,
- OnCellValidation,
- RowValue,
- EDIT_COL,
- OnRowDeletion,
- addDeleteColumn,
- headBoxSx,
- getClassName,
- LINE_STYLE,
- iconInRowSx,
- DEFAULT_SIZE,
- OnRowSelection,
- getRowIndex,
- getTooltip,
- defaultColumns,
- OnRowClick,
- DownloadAction,
- } from "./tableUtils";
- import {
- useClassNames,
- useDispatch,
- useDispatchRequestUpdateOnFirstRender,
- useDynamicJsonProperty,
- useDynamicProperty,
- useFormatConfig,
- useModule,
- } from "../../utils/hooks";
- import TableFilter, { FilterDesc } from "./TableFilter";
- import { getSuffixedClassNames, getUpdateVar } from "./utils";
- interface RowData {
- colsOrder: string[];
- columns: Record<string, ColumnDesc>;
- rows: RowType[];
- classes: Record<string, string>;
- cellProps: Partial<TableCellProps>[];
- isItemLoaded: (index: number) => boolean;
- selection: number[];
- formatConfig: FormatConfig;
- onValidation?: OnCellValidation;
- onDeletion?: OnRowDeletion;
- onRowSelection?: OnRowSelection;
- onRowClick?: OnRowClick;
- lineStyle?: string;
- nanValue?: string;
- compRows?: RowType[];
- }
- const Row = ({
- index,
- style,
- data: {
- colsOrder,
- columns,
- rows,
- classes,
- cellProps,
- isItemLoaded,
- selection,
- formatConfig,
- onValidation,
- onDeletion,
- onRowSelection,
- onRowClick,
- lineStyle,
- nanValue,
- compRows,
- },
- }: {
- index: number;
- style: CSSProperties;
- data: RowData;
- }) =>
- isItemLoaded(index) ? (
- <TableRow
- hover
- tabIndex={-1}
- key={"row" + index}
- component="div"
- sx={style}
- className={(classes && classes.row) + " " + getClassName(rows[index], lineStyle)}
- data-index={index}
- selected={selection.indexOf(index) > -1}
- onClick={onRowClick}
- >
- {colsOrder.map((col, cidx) => (
- <EditableCell
- key={"val" + index + "-" + cidx}
- className={getClassName(rows[index], columns[col].style, col)}
- colDesc={columns[col]}
- value={rows[index][col]}
- formatConfig={formatConfig}
- rowIndex={index}
- onValidation={!columns[col].notEditable ? onValidation : undefined}
- onDeletion={onDeletion}
- onSelection={onRowSelection}
- nanValue={columns[col].nanValue || nanValue}
- tableCellProps={cellProps[cidx]}
- tooltip={getTooltip(rows[index], columns[col].tooltip, col)}
- comp={compRows && compRows[index] && compRows[index][col]}
- />
- ))}
- </TableRow>
- ) : (
- <Skeleton sx={style} key={"Skeleton" + index} />
- );
- interface PromiseProps {
- resolve: () => void;
- reject: () => void;
- }
- interface key2Rows {
- key: string;
- promises: Record<number, PromiseProps>;
- }
- const getRowHeight = (size = DEFAULT_SIZE) => (size == DEFAULT_SIZE ? 37 : 54);
- const getCellSx = (width: string | number | undefined, size = DEFAULT_SIZE) => ({
- width: width,
- height: 22,
- padding: size == DEFAULT_SIZE ? "7px" : undefined,
- });
- const AutoLoadingTable = (props: TaipyTableProps) => {
- const {
- id,
- updateVarName,
- height = "80vh",
- width = "100%",
- updateVars,
- selected = [],
- pageSize = 100,
- defaultKey = "",
- onEdit = "",
- onDelete = "",
- onAdd = "",
- onAction = "",
- size = DEFAULT_SIZE,
- userData,
- downloadable = false,
- compare = false,
- onCompare = "",
- } = props;
- const [rows, setRows] = useState<RowType[]>([]);
- const [compRows, setCompRows] = useState<RowType[]>([]);
- const [rowCount, setRowCount] = useState(1000); // need something > 0 to bootstrap the infinite loader
- const [filteredCount, setFilteredCount] = useState(0);
- const dispatch = useDispatch();
- const page = useRef<key2Rows>({ key: defaultKey, promises: {} });
- const [orderBy, setOrderBy] = useState("");
- const [order, setOrder] = useState<Order>("asc");
- const [appliedFilters, setAppliedFilters] = useState<FilterDesc[]>([]);
- const [visibleStartIndex, setVisibleStartIndex] = useState(0);
- const [aggregates, setAggregates] = useState<string[]>([]);
- const infiniteLoaderRef = useRef<InfiniteLoader>(null);
- const headerRow = useRef<HTMLTableRowElement>(null);
- const formatConfig = useFormatConfig();
- const module = useModule();
- const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
- const active = useDynamicProperty(props.active, props.defaultActive, true);
- const editable = useDynamicProperty(props.editable, props.defaultEditable, true);
- const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
- const baseColumns = useDynamicJsonProperty(props.columns, props.defaultColumns, defaultColumns);
- const refresh = typeof props.data === "number";
- useEffect(() => {
- if (!refresh && props.data && page.current.key && props.data[page.current.key] !== undefined) {
- const newValue = props.data[page.current.key];
- const promise = page.current.promises[newValue.start];
- setRowCount(newValue.rowcount);
- setFilteredCount(
- newValue.fullrowcount && newValue.rowcount != newValue.fullrowcount
- ? newValue.fullrowcount - newValue.rowcount
- : 0
- );
- const nr = newValue.data as RowType[];
- if (Array.isArray(nr) && nr.length > newValue.start) {
- setRows(nr);
- newValue.comp && setCompRows(newValue.comp as RowType[])
- promise && promise.resolve();
- } else {
- promise && promise.reject();
- }
- delete page.current.promises[newValue.start];
- }
- }, [refresh, props.data]);
- useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
- const onSort = useCallback(
- (e: React.MouseEvent<HTMLElement>) => {
- const col = e.currentTarget.getAttribute("data-dfid");
- if (col) {
- const isAsc = orderBy === col && order === "asc";
- setOrder(isAsc ? "desc" : "asc");
- setOrderBy(col);
- setRows([]);
- setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
- }
- },
- [orderBy, order]
- );
- useEffect(() => {
- if (refresh) {
- setRows([]);
- setTimeout(() => infiniteLoaderRef.current?.resetloadMoreItemsCache(true), 1); // So that the state can be changed
- }
- }, [refresh]);
- 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 [colsOrder, columns, styles, tooltips, handleNan, filter] = useMemo(() => {
- let hNan = !!props.nanValue;
- if (baseColumns) {
- try {
- let filter = false;
- Object.values(baseColumns).forEach((col) => {
- if (typeof col.filter != "boolean") {
- col.filter = !!props.filter;
- }
- filter = filter || col.filter;
- if (typeof col.notEditable != "boolean") {
- col.notEditable = !editable;
- }
- if (col.tooltip === undefined) {
- col.tooltip = props.tooltip;
- }
- });
- addDeleteColumn(
- (active && (onAdd || onDelete) ? 1 : 0) +
- (active && filter ? 1 : 0) +
- (active && downloadable ? 1 : 0),
- baseColumns
- );
- const colsOrder = Object.keys(baseColumns).sort(getsortByIndex(baseColumns));
- const styTt = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
- if (baseColumns[col].style) {
- pv.styles = pv.styles || {};
- pv.styles[baseColumns[col].dfid] = baseColumns[col].style as string;
- }
- hNan = hNan || !!baseColumns[col].nanValue;
- if (baseColumns[col].tooltip) {
- pv.tooltips = pv.tooltips || {};
- pv.tooltips[baseColumns[col].dfid] = baseColumns[col].tooltip as string;
- }
- return pv;
- }, {});
- if (props.lineStyle) {
- styTt.styles = styTt.styles || {};
- styTt.styles[LINE_STYLE] = props.lineStyle;
- }
- return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
- } catch (e) {
- console.info("ATable.columns: " + ((e as Error).message || e));
- }
- }
- return [
- [],
- {} as Record<string, ColumnDesc>,
- {} as Record<string, string>,
- {} as Record<string, string>,
- hNan,
- false,
- ];
- }, [
- active,
- editable,
- onAdd,
- onDelete,
- baseColumns,
- props.lineStyle,
- props.tooltip,
- props.nanValue,
- props.filter,
- downloadable,
- ]);
- const boxBodySx = useMemo(() => ({ height: height }), [height]);
- useEffect(() => {
- selected.length &&
- infiniteLoaderRef.current &&
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (infiniteLoaderRef.current as any)._listRef.scrollToItem(selected[0]);
- }, [selected]);
- useEffect(() => {
- if (headerRow.current) {
- Array.from(headerRow.current.cells).forEach((cell, idx) => {
- columns[colsOrder[idx]].widthHint = cell.offsetWidth;
- });
- }
- }, [columns, colsOrder]);
- const loadMoreItems = useCallback(
- (startIndex: number, stopIndex: number) => {
- if (page.current.promises[startIndex]) {
- page.current.promises[startIndex].reject();
- }
- return new Promise<void>((resolve, reject) => {
- const agg = aggregates.length
- ? colsOrder.reduce((pv, col, idx) => {
- if (aggregates.includes(columns[col].dfid)) {
- return pv + "-" + idx;
- }
- return pv;
- }, "-agg")
- : "";
- 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 = `Infinite-${cols.join()}-${orderBy}-${order}${agg}${afs.map(
- (af) => `${af.col}${af.action}${af.value}`
- )}`;
- page.current = {
- key: key,
- promises: { ...page.current.promises, [startIndex]: { resolve: resolve, reject: reject } },
- };
- 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(
- createRequestInfiniteTableUpdateAction(
- updateVarName,
- id,
- module,
- cols,
- key,
- startIndex,
- stopIndex,
- orderBy,
- order,
- aggregates,
- applies,
- styles,
- tooltips,
- handleNan,
- afs,
- compare ? onCompare : undefined,
- updateVars && getUpdateVar(updateVars, "comparedatas")
- )
- );
- });
- },
- [
- aggregates,
- styles,
- tooltips,
- updateVarName,
- updateVars,
- orderBy,
- order,
- id,
- colsOrder,
- columns,
- handleNan,
- appliedFilters,
- compare,
- onCompare,
- dispatch,
- module,
- ]
- );
- const onAddRowClick = useCallback(
- () =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onAdd,
- index: visibleStartIndex,
- user_data: userData,
- })
- ),
- [visibleStartIndex, dispatch, updateVarName, onAdd, module, userData]
- );
- const onDownload = useCallback(
- () =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: DownloadAction,
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, module, userData]
- );
- const isItemLoaded = useCallback((index: number) => index < rows.length && !!rows[index], [rows]);
- const onCellValidation: OnCellValidation = useCallback(
- (value: RowValue, rowIndex: number, colName: string, userValue: string, tz?: string) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onEdit,
- value: value,
- index: getRowIndex(rows[rowIndex], rowIndex),
- col: colName,
- user_value: userValue,
- tz: tz,
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onEdit, rows, module, userData]
- );
- const onRowDeletion: OnRowDeletion = useCallback(
- (rowIndex: number) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onDelete,
- index: getRowIndex(rows[rowIndex], rowIndex),
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onDelete, rows, module, userData]
- );
- const onRowSelection: OnRowSelection = useCallback(
- (rowIndex: number, colName?: string, value?: string) =>
- dispatch(
- createSendActionNameAction(updateVarName, module, {
- action: onAction,
- index: getRowIndex(rows[rowIndex], rowIndex),
- col: colName === undefined ? null : colName,
- value,
- reason: value === undefined ? "click" : "button",
- user_data: userData,
- })
- ),
- [dispatch, updateVarName, onAction, rows, module, userData]
- );
- const onRowClick = useCallback(
- (e: MouseEvent<HTMLTableRowElement>) => {
- const { index } = e.currentTarget.dataset || {};
- const rowIndex = index === undefined ? NaN : Number(index);
- if (!isNaN(rowIndex)) {
- onRowSelection(rowIndex);
- }
- },
- [onRowSelection]
- );
- const onTaipyItemsRendered = useCallback(
- (onItemsR: (props: ListOnItemsRenderedProps) => undefined) =>
- ({ visibleStartIndex, visibleStopIndex }: { visibleStartIndex: number; visibleStopIndex: number }) => {
- setVisibleStartIndex(visibleStartIndex);
- onItemsR({ visibleStartIndex, visibleStopIndex } as ListOnItemsRenderedProps);
- },
- []
- );
- const rowData: RowData = useMemo(
- () => ({
- colsOrder: colsOrder,
- columns: columns,
- rows: rows,
- classes: {},
- cellProps: colsOrder.map((col) => ({
- sx: getCellSx(columns[col].width || columns[col].widthHint, size),
- component: "div",
- variant: "body",
- })),
- isItemLoaded: isItemLoaded,
- selection: selected,
- formatConfig: formatConfig,
- onValidation: active && onEdit ? onCellValidation : undefined,
- onDeletion: active && onDelete ? onRowDeletion : undefined,
- onRowSelection: active && onAction ? onRowSelection : undefined,
- onRowClick: active && onAction ? onRowClick : undefined,
- lineStyle: props.lineStyle,
- nanValue: props.nanValue,
- compRows: compRows,
- }),
- [
- rows,
- compRows,
- isItemLoaded,
- active,
- colsOrder,
- columns,
- selected,
- formatConfig,
- onEdit,
- onCellValidation,
- onDelete,
- onRowDeletion,
- onAction,
- onRowSelection,
- onRowClick,
- props.lineStyle,
- props.nanValue,
- size,
- ]
- );
- const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]);
- return (
- <Box id={id} sx={boxSx} className={`${className} ${getSuffixedClassNames(className, "-autoloading")}`}>
- <Paper sx={paperSx}>
- <Tooltip title={hover || ""}>
- <TableContainer>
- <MuiTable sx={tableSx} aria-labelledby="tableTitle" size={size} stickyHeader={true}>
- <TableHead>
- <TableRow ref={headerRow}>
- {colsOrder.map((col, idx) => (
- <TableCell
- key={col + idx}
- sortDirection={orderBy === columns[col].dfid && order}
- sx={columns[col].width ? { width: columns[col].width } : {}}
- >
- {columns[col].dfid === EDIT_COL ? (
- [
- active && 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>
- </MuiTable>
- <Box sx={boxBodySx}>
- <AutoSizer>
- {({ height, width }) => (
- <InfiniteLoader
- ref={infiniteLoaderRef}
- isItemLoaded={isItemLoaded}
- itemCount={rowCount}
- loadMoreItems={loadMoreItems}
- minimumBatchSize={pageSize}
- >
- {({ onItemsRendered, ref }) => (
- <FixedSizeList
- height={height || 100}
- width={width || 100}
- itemCount={rowCount}
- itemSize={getRowHeight(size)}
- onItemsRendered={onTaipyItemsRendered(onItemsRendered)}
- ref={ref}
- itemData={rowData}
- >
- {Row}
- </FixedSizeList>
- )}
- </InfiniteLoader>
- )}
- </AutoSizer>
- </Box>
- </TableContainer>
- </Tooltip>
- </Paper>
- </Box>
- );
- };
- export default AutoLoadingTable;
|