1
0

PaginatedTable.tsx 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. /*
  2. * Copyright 2021-2025 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, {
  14. ChangeEvent,
  15. CSSProperties,
  16. MouseEvent,
  17. useCallback,
  18. useEffect,
  19. useMemo,
  20. useRef,
  21. useState,
  22. } from "react";
  23. import AddIcon from "@mui/icons-material/Add";
  24. import DataSaverOff from "@mui/icons-material/DataSaverOff";
  25. import DataSaverOn from "@mui/icons-material/DataSaverOn";
  26. import Download from "@mui/icons-material/Download";
  27. import Box from "@mui/material/Box";
  28. import IconButton from "@mui/material/IconButton";
  29. import Paper from "@mui/material/Paper";
  30. import Skeleton from "@mui/material/Skeleton";
  31. import Table from "@mui/material/Table";
  32. import TableBody from "@mui/material/TableBody";
  33. import TableCell from "@mui/material/TableCell";
  34. import TableContainer from "@mui/material/TableContainer";
  35. import TableHead from "@mui/material/TableHead";
  36. import TablePagination from "@mui/material/TablePagination";
  37. import TableRow from "@mui/material/TableRow";
  38. import TableSortLabel from "@mui/material/TableSortLabel";
  39. import Tooltip from "@mui/material/Tooltip";
  40. import Typography from "@mui/material/Typography";
  41. import { visuallyHidden } from "@mui/utils";
  42. import { createRequestTableUpdateAction, createSendActionNameAction } from "../../context/taipyReducers";
  43. import { emptyArray } from "../../utils";
  44. import {
  45. useClassNames,
  46. useDispatch,
  47. useDispatchRequestUpdateOnFirstRender,
  48. useDynamicJsonProperty,
  49. useDynamicProperty,
  50. useFormatConfig,
  51. useModule,
  52. } from "../../utils/hooks";
  53. import TableFilter from "./TableFilter";
  54. import {
  55. addActionColumn,
  56. baseBoxSx,
  57. ColumnDesc,
  58. DEFAULT_SIZE,
  59. defaultColumns,
  60. DownloadAction,
  61. EDIT_COL,
  62. EditableCell,
  63. FilterDesc,
  64. generateHeaderClassName,
  65. getClassName,
  66. getColumnHeader,
  67. getFormatFn,
  68. getPageKey,
  69. getRowIndex,
  70. getSortByIndex,
  71. getTooltip,
  72. headBoxSx,
  73. iconInRowSx,
  74. OnCellValidation,
  75. OnRowClick,
  76. OnRowDeletion,
  77. OnRowSelection,
  78. Order,
  79. PageSizeOptionsType,
  80. paperSx,
  81. ROW_CLASS_NAME,
  82. RowType,
  83. RowValue,
  84. tableSx,
  85. TaipyPaginatedTableProps,
  86. } from "./tableUtils";
  87. import { getComponentClassName } from "./TaipyStyle";
  88. import { getCssSize, getSuffixedClassNames, getUpdateVar } from "./utils";
  89. const loadingStyle: CSSProperties = { width: "100%", height: "3em", textAlign: "right", verticalAlign: "center" };
  90. const skeletonSx = { width: "100%", height: "3em" };
  91. const rowsPerPageOptions: PageSizeOptionsType = [10, 50, 100, 500];
  92. const PaginatedTable = (props: TaipyPaginatedTableProps) => {
  93. const {
  94. id,
  95. updateVarName,
  96. pageSizeOptions,
  97. allowAllRows = false,
  98. showAll = false,
  99. height,
  100. selected = emptyArray,
  101. updateVars,
  102. onEdit = "",
  103. onDelete = "",
  104. onAdd = "",
  105. onAction = "",
  106. width = "100%",
  107. size = DEFAULT_SIZE,
  108. userData,
  109. downloadable = false,
  110. compare = false,
  111. onCompare = "",
  112. useCheckbox = false,
  113. sortable = true,
  114. } = props;
  115. const pageSize = props.pageSize === undefined || props.pageSize < 1 ? 100 : Math.round(props.pageSize);
  116. const [value, setValue] = useState<Record<string, unknown>>({});
  117. const [startIndex, setStartIndex] = useState(0);
  118. const [rowsPerPage, setRowsPerPage] = useState(pageSize);
  119. const [order, setOrder] = useState<Order>("asc");
  120. const [orderBy, setOrderBy] = useState("");
  121. const [loading, setLoading] = useState(true);
  122. const [aggregates, setAggregates] = useState<string[]>([]);
  123. const [appliedFilters, setAppliedFilters] = useState<FilterDesc[]>([]);
  124. const dispatch = useDispatch();
  125. const pageKey = useRef("no-page");
  126. const selectedRowRef = useRef<HTMLTableRowElement | null>(null);
  127. const formatConfig = useFormatConfig();
  128. const module = useModule();
  129. const refresh = props.data?.__taipy_refresh !== undefined;
  130. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  131. const active = useDynamicProperty(props.active, props.defaultActive, true);
  132. const editable = useDynamicProperty(props.editable, props.defaultEditable, false);
  133. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  134. const baseColumns = useDynamicJsonProperty(props.columns, props.defaultColumns, defaultColumns);
  135. const [
  136. colsOrder,
  137. columns,
  138. cellClassNames,
  139. tooltips,
  140. formats,
  141. handleNan,
  142. filter,
  143. partialEditable,
  144. calcWidth,
  145. nbColHeaders,
  146. headersInfo,
  147. ] = useMemo(() => {
  148. let hNan = !!props.nanValue;
  149. let nbColHeaders = 1;
  150. if (baseColumns) {
  151. try {
  152. let filter = false;
  153. let partialEditable = editable;
  154. const newCols: Record<string, ColumnDesc> = {};
  155. Object.entries(baseColumns).forEach(([cId, cDesc]) => {
  156. const nDesc = (newCols[cId] = { ...cDesc });
  157. if (typeof nDesc.filter != "boolean") {
  158. nDesc.filter = !!props.filter;
  159. }
  160. filter = filter || nDesc.filter;
  161. if (typeof nDesc.notEditable == "boolean") {
  162. partialEditable = partialEditable || !nDesc.notEditable;
  163. } else {
  164. nDesc.notEditable = !editable;
  165. }
  166. if (nDesc.tooltip === undefined) {
  167. nDesc.tooltip = props.tooltip;
  168. }
  169. if (nDesc.multi !== undefined) {
  170. nDesc.sortable = false;
  171. } else if (typeof nDesc.sortable != "boolean") {
  172. nDesc.sortable = sortable;
  173. }
  174. nbColHeaders = Math.max(nbColHeaders, nDesc.headers?.length || 0);
  175. });
  176. addActionColumn(
  177. (active && partialEditable && (onAdd || onDelete) ? 1 : 0) +
  178. (active && filter ? 1 : 0) +
  179. (active && downloadable ? 1 : 0),
  180. newCols
  181. );
  182. const colsOrder = Object.keys(newCols).sort(getSortByIndex(newCols));
  183. const headersInfo = [];
  184. if (nbColHeaders > 1) {
  185. for (let i = 0; i < nbColHeaders; i++) {
  186. const headers = colsOrder.map((col, idx) => {
  187. const header = getColumnHeader(newCols, col, i);
  188. return idx > 0 && header === getColumnHeader(newCols, colsOrder[idx - 1], i)
  189. ? undefined
  190. : header;
  191. });
  192. const colSpans = headers.map((header, idx) => {
  193. if (header === undefined) {
  194. return 0;
  195. }
  196. const nh = headers.slice(idx + 1);
  197. const nb = nh.findIndex((h) => h !== undefined);
  198. return nb == -1 ? nh.length + 1 : nb + 1;
  199. });
  200. headersInfo.push({ headers, colSpans });
  201. }
  202. }
  203. let nbWidth = 0;
  204. let widthRate = 0;
  205. const functions = colsOrder.reduce<Record<string, Record<string, string>>>((pv, col) => {
  206. if (newCols[col].className) {
  207. pv.classNames = pv.classNames || {};
  208. pv.classNames[newCols[col].dfid] = newCols[col].className;
  209. }
  210. hNan = hNan || !!newCols[col].nanValue;
  211. if (newCols[col].tooltip) {
  212. pv.tooltips = pv.tooltips || {};
  213. pv.tooltips[newCols[col].dfid] = newCols[col].tooltip;
  214. }
  215. if (newCols[col].formatFn) {
  216. pv.formats = pv.formats || {};
  217. pv.formats[newCols[col].dfid] = newCols[col].formatFn;
  218. }
  219. if (newCols[col].width !== undefined) {
  220. const cssWidth = getCssSize(newCols[col].width);
  221. if (cssWidth) {
  222. newCols[col].width = cssWidth;
  223. nbWidth++;
  224. if (cssWidth.endsWith("%")) {
  225. widthRate += parseInt(cssWidth, 10);
  226. }
  227. }
  228. }
  229. return pv;
  230. }, {});
  231. nbWidth = nbWidth ? colsOrder.length - nbWidth : 0;
  232. if (props.rowClassName) {
  233. functions.classNames = functions.classNames || {};
  234. functions.classNames[ROW_CLASS_NAME] = props.rowClassName;
  235. }
  236. return [
  237. colsOrder,
  238. newCols,
  239. functions.classNames,
  240. functions.tooltips,
  241. functions.formats,
  242. hNan,
  243. filter,
  244. partialEditable,
  245. nbWidth > 0 ? `${(100 - widthRate) / nbWidth}%` : undefined,
  246. nbColHeaders,
  247. headersInfo,
  248. ];
  249. } catch (e) {
  250. console.info("PaginatedTable.columns: ", (e as Error).message || e);
  251. }
  252. }
  253. return [
  254. [] as string[],
  255. {} as Record<string, ColumnDesc>,
  256. {} as Record<string, string>,
  257. {} as Record<string, string>,
  258. {} as Record<string, string>,
  259. hNan,
  260. false,
  261. false,
  262. "",
  263. 1,
  264. [],
  265. ];
  266. }, [
  267. active,
  268. editable,
  269. onAdd,
  270. onDelete,
  271. baseColumns,
  272. props.rowClassName,
  273. props.tooltip,
  274. props.nanValue,
  275. props.filter,
  276. downloadable,
  277. sortable,
  278. ]);
  279. useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
  280. /*
  281. TODO: If the 'selected' value is a negative number, it will lead to unexpected pagination behavior.
  282. For instance, if 'selected' is -1, the pagination will display from -99 to 0 and no data will be selected.
  283. Need to fix this issue.
  284. */
  285. useEffect(() => {
  286. if (selected.length) {
  287. if (selected[0] < startIndex || selected[0] > startIndex + rowsPerPage) {
  288. setLoading(true);
  289. setStartIndex(rowsPerPage * Math.floor(selected[0] / rowsPerPage));
  290. }
  291. }
  292. }, [selected, startIndex, rowsPerPage]);
  293. useEffect(() => {
  294. if (!refresh && props.data && props.data[pageKey.current] !== undefined) {
  295. setValue(props.data[pageKey.current]);
  296. setLoading(false);
  297. }
  298. }, [refresh, props.data]);
  299. useEffect(() => {
  300. const endIndex = showAll ? -1 : startIndex + rowsPerPage - 1;
  301. const cols = colsOrder.map((col) => columns[col].dfid).filter((c) => c != EDIT_COL);
  302. const afs = appliedFilters.filter((fd) => Object.values(columns).some((cd) => cd.dfid === fd.col));
  303. pageKey.current = getPageKey(
  304. columns,
  305. `${startIndex}-${endIndex}`,
  306. cols,
  307. orderBy,
  308. order,
  309. afs,
  310. aggregates,
  311. cellClassNames,
  312. tooltips,
  313. formats
  314. );
  315. if (refresh || !props.data || props.data[pageKey.current] === undefined) {
  316. setLoading(true);
  317. const applies = aggregates.length
  318. ? colsOrder.reduce<Record<string, unknown>>((pv, col) => {
  319. if (columns[col].apply) {
  320. pv[columns[col].dfid] = columns[col].apply;
  321. }
  322. return pv;
  323. }, {})
  324. : undefined;
  325. dispatch(
  326. createRequestTableUpdateAction(
  327. updateVarName,
  328. id,
  329. module,
  330. cols,
  331. pageKey.current,
  332. startIndex,
  333. endIndex,
  334. orderBy,
  335. order,
  336. aggregates,
  337. applies,
  338. cellClassNames,
  339. tooltips,
  340. formats,
  341. handleNan,
  342. afs,
  343. compare ? onCompare : undefined,
  344. updateVars && getUpdateVar(updateVars, "comparedatas"),
  345. typeof userData == "object"
  346. ? (userData as Record<string, Record<string, unknown>>).context
  347. : undefined
  348. )
  349. );
  350. } else {
  351. setValue(props.data[pageKey.current]);
  352. setLoading(false);
  353. }
  354. // eslint-disable-next-line react-hooks/exhaustive-deps
  355. }, [
  356. refresh,
  357. startIndex,
  358. aggregates,
  359. colsOrder,
  360. columns,
  361. showAll,
  362. cellClassNames,
  363. tooltips,
  364. formats,
  365. rowsPerPage,
  366. order,
  367. orderBy,
  368. updateVarName,
  369. updateVars,
  370. id,
  371. handleNan,
  372. appliedFilters,
  373. dispatch,
  374. module,
  375. compare,
  376. onCompare,
  377. userData,
  378. ]);
  379. const onSort = useCallback(
  380. (e: MouseEvent<HTMLElement>) => {
  381. const col = e.currentTarget.getAttribute("data-dfid");
  382. if (col) {
  383. const isAsc = orderBy === col && order === "asc";
  384. setOrder(isAsc ? "desc" : "asc");
  385. setOrderBy(col);
  386. }
  387. },
  388. [orderBy, order]
  389. );
  390. const handleChangePage = useCallback(
  391. (event: unknown, newPage: number) => {
  392. setStartIndex(newPage * rowsPerPage);
  393. },
  394. [rowsPerPage]
  395. );
  396. const handleChangeRowsPerPage = useCallback((event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  397. setLoading(true);
  398. setRowsPerPage(parseInt(event.target.value, 10));
  399. setStartIndex(0);
  400. }, []);
  401. const onAggregate = useCallback((e: MouseEvent<HTMLElement>) => {
  402. const groupBy = e.currentTarget.getAttribute("data-dfid");
  403. if (groupBy) {
  404. setAggregates((ags) => {
  405. const nags = ags.filter((ag) => ag !== groupBy);
  406. if (ags.length == nags.length) {
  407. nags.push(groupBy);
  408. }
  409. return nags;
  410. });
  411. }
  412. e.stopPropagation();
  413. }, []);
  414. const onAddRowClick = useCallback(
  415. () =>
  416. dispatch(
  417. createSendActionNameAction(updateVarName, module, {
  418. action: onAdd,
  419. index: startIndex,
  420. user_data: userData,
  421. })
  422. ),
  423. [startIndex, dispatch, updateVarName, onAdd, module, userData]
  424. );
  425. const onDownload = useCallback(
  426. () =>
  427. dispatch(
  428. createSendActionNameAction(updateVarName, module, {
  429. action: DownloadAction,
  430. user_data: userData,
  431. })
  432. ),
  433. [dispatch, updateVarName, module, userData]
  434. );
  435. const tableContainerSx = useMemo(() => ({ maxHeight: height }), [height]);
  436. const pso = useMemo(() => {
  437. let psOptions = rowsPerPageOptions;
  438. if (pageSizeOptions) {
  439. try {
  440. psOptions = JSON.parse(pageSizeOptions);
  441. } catch (e) {
  442. console.log("PaginatedTable pageSizeOptions is wrong ", pageSizeOptions, e);
  443. }
  444. }
  445. if (
  446. pageSize > 0 &&
  447. !psOptions.some((ps) =>
  448. typeof ps === "number" ? ps === pageSize : typeof ps.value === "number" ? ps.value === pageSize : false
  449. )
  450. ) {
  451. psOptions.push({ value: pageSize, label: "" + pageSize });
  452. }
  453. if (allowAllRows) {
  454. psOptions.push({ value: -1, label: "All" });
  455. }
  456. psOptions.sort((a, b) => (typeof a === "number" ? a : a.value) - (typeof b === "number" ? b : b.value));
  457. return psOptions;
  458. }, [pageSizeOptions, allowAllRows, pageSize]);
  459. const { rows, rowCount, filteredCount, compRows } = useMemo(() => {
  460. const ret = { rows: undefined, rowCount: 0, filteredCount: 0, compRows: [] } as {
  461. rows?: RowType[];
  462. rowCount: number;
  463. filteredCount: number;
  464. compRows: RowType[];
  465. };
  466. if (value) {
  467. if (value.data) {
  468. ret.rows = value.data as RowType[];
  469. }
  470. if (value.rowcount) {
  471. ret.rowCount = value.rowcount as unknown as number;
  472. if (value.fullrowcount && value.rowcount != value.fullrowcount) {
  473. ret.filteredCount = (value.fullrowcount as unknown as number) - ret.rowCount;
  474. }
  475. }
  476. if (value.comp) {
  477. ret.compRows = value.comp as RowType[];
  478. }
  479. }
  480. return ret;
  481. }, [value]);
  482. const onCellValidation: OnCellValidation = useCallback(
  483. (value: RowValue, rowIndex: number, colName: string, userValue: string, tz?: string) =>
  484. dispatch(
  485. createSendActionNameAction(updateVarName, module, {
  486. action: onEdit,
  487. value: value,
  488. index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex) : startIndex,
  489. col: colName,
  490. user_value: userValue,
  491. tz: tz,
  492. user_data: userData,
  493. })
  494. ),
  495. [dispatch, updateVarName, onEdit, rows, startIndex, module, userData]
  496. );
  497. const onRowDeletion: OnRowDeletion = useCallback(
  498. (rowIndex: number) =>
  499. dispatch(
  500. createSendActionNameAction(updateVarName, module, {
  501. action: onDelete,
  502. index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex) : startIndex,
  503. user_data: userData,
  504. })
  505. ),
  506. [dispatch, updateVarName, onDelete, rows, startIndex, module, userData]
  507. );
  508. const onRowSelection: OnRowSelection = useCallback(
  509. (rowIndex: number, colName?: string, value?: string) =>
  510. dispatch(
  511. createSendActionNameAction(updateVarName, module, {
  512. action: onAction,
  513. index: rows ? getRowIndex(rows[rowIndex], rowIndex, startIndex) : startIndex,
  514. col: colName === undefined ? null : colName,
  515. value,
  516. reason: value === undefined ? "click" : "button",
  517. user_data: userData,
  518. })
  519. ),
  520. [dispatch, updateVarName, onAction, rows, startIndex, module, userData]
  521. );
  522. const onRowClick: OnRowClick = useCallback(
  523. (e: MouseEvent<HTMLTableRowElement>) => {
  524. const { index } = e.currentTarget.dataset || {};
  525. const rowIndex = index === undefined ? NaN : Number(index);
  526. if (!isNaN(rowIndex)) {
  527. onRowSelection(rowIndex);
  528. }
  529. },
  530. [onRowSelection]
  531. );
  532. const boxSx = useMemo(() => ({ ...baseBoxSx, width: width }), [width]);
  533. const rowSpans = useMemo(
  534. () =>
  535. rows
  536. ? colsOrder
  537. .filter((col) => columns[col].multi !== undefined)
  538. .map((col) => {
  539. const values = rows.map((r, idx) =>
  540. idx > 0 && rows[idx - 1][col] == r[col] ? undefined : r[col]
  541. );
  542. return values.map((value, idx) => {
  543. if (value === undefined) {
  544. return 0;
  545. }
  546. const nv = values.slice(idx + 1);
  547. const nb = nv.findIndex((v) => v !== undefined);
  548. return nb == -1 ? nv.length + 1 : nb + 1;
  549. });
  550. })
  551. : [],
  552. [colsOrder, columns, rows]
  553. );
  554. return (
  555. <Box
  556. id={id}
  557. sx={boxSx}
  558. className={`${className} ${getSuffixedClassNames(className, "-paginated")} ${getComponentClassName(
  559. props.children
  560. )}`}
  561. >
  562. <Paper sx={paperSx}>
  563. <Tooltip title={hover || ""}>
  564. <TableContainer sx={tableContainerSx}>
  565. <Table sx={tableSx} aria-labelledby="tableTitle" size={size} stickyHeader={true}>
  566. <TableHead>
  567. {Array.from(Array(nbColHeaders).keys()).map((idx) => {
  568. if (idx < nbColHeaders - 1) {
  569. return (
  570. <TableRow key={`rowheader${idx}`}>
  571. {colsOrder.map((col, i) => {
  572. const colSpan =
  573. headersInfo[idx] && headersInfo[idx].colSpans.length > i
  574. ? headersInfo[idx].colSpans[i]
  575. : 1;
  576. return colSpan == 0 ? null : (
  577. <TableCell
  578. colSpan={colSpan}
  579. key={`head${columns[col].dfid}`}
  580. sx={
  581. columns[col].width
  582. ? { minWidth: columns[col].width, maxWidth: columns[col].width }
  583. : calcWidth
  584. ? { width: calcWidth }
  585. : undefined
  586. }
  587. className={
  588. col === "EDIT_COL"
  589. ? getSuffixedClassNames(className, "-action")
  590. : getSuffixedClassNames(
  591. className,
  592. generateHeaderClassName(columns[col].dfid)
  593. )
  594. }
  595. >
  596. {(headersInfo[idx] &&
  597. headersInfo[idx].headers.length > i &&
  598. headersInfo[idx].headers[i]) ||
  599. ""}
  600. </TableCell>
  601. );
  602. })}
  603. </TableRow>
  604. );
  605. } else {
  606. return (
  607. <TableRow key={`rowheader${idx}`}>
  608. {colsOrder.map((col, i) => (
  609. <TableCell
  610. key={`head${columns[col].dfid}`}
  611. sortDirection={orderBy === columns[col].dfid && order}
  612. sx={
  613. columns[col].width
  614. ? { minWidth: columns[col].width, maxWidth: columns[col].width }
  615. : calcWidth
  616. ? { width: calcWidth }
  617. : undefined
  618. }
  619. className={
  620. col === "EDIT_COL"
  621. ? getSuffixedClassNames(className, "-action")
  622. : getSuffixedClassNames(
  623. className,
  624. generateHeaderClassName(columns[col].dfid)
  625. )
  626. }
  627. >
  628. {columns[col].dfid === EDIT_COL ? (
  629. [
  630. active && (editable || partialEditable) && onAdd ? (
  631. <Tooltip title="Add a row" key="addARow">
  632. <IconButton
  633. onClick={onAddRowClick}
  634. size="small"
  635. sx={iconInRowSx}
  636. >
  637. <AddIcon fontSize="inherit" />
  638. </IconButton>
  639. </Tooltip>
  640. ) : null,
  641. active && filter ? (
  642. <TableFilter
  643. key="filter"
  644. columns={columns}
  645. colsOrder={colsOrder}
  646. onValidate={setAppliedFilters}
  647. appliedFilters={appliedFilters}
  648. className={className}
  649. filteredCount={filteredCount}
  650. />
  651. ) : null,
  652. active && downloadable ? (
  653. <Tooltip title="Download as CSV" key="downloadCsv">
  654. <IconButton
  655. onClick={onDownload}
  656. size="small"
  657. sx={iconInRowSx}
  658. >
  659. <Download fontSize="inherit" />
  660. </IconButton>
  661. </Tooltip>
  662. ) : null,
  663. ]
  664. ) : (
  665. <TableSortLabel
  666. active={orderBy === columns[col].dfid}
  667. direction={
  668. orderBy === columns[col].dfid ? order : "asc"
  669. }
  670. data-dfid={columns[col].dfid}
  671. onClick={onSort}
  672. disabled={!active || !columns[col].sortable}
  673. hideSortIcon={!active || !columns[col].sortable}
  674. >
  675. <Box sx={headBoxSx}>
  676. {columns[col].groupBy ? (
  677. <IconButton
  678. onClick={onAggregate}
  679. size="small"
  680. title="aggregate"
  681. data-dfid={columns[col].dfid}
  682. disabled={!active}
  683. sx={iconInRowSx}
  684. >
  685. {aggregates.includes(columns[col].dfid) ? (
  686. <DataSaverOff fontSize="inherit" />
  687. ) : (
  688. <DataSaverOn fontSize="inherit" />
  689. )}
  690. </IconButton>
  691. ) : null}
  692. {columns[col].title === undefined
  693. ? (headersInfo[idx] &&
  694. headersInfo[idx].headers.length > i &&
  695. headersInfo[idx].headers[i]) ||
  696. columns[col].dfid
  697. : columns[col].title}
  698. </Box>
  699. {orderBy === columns[col].dfid ? (
  700. <Box component="span" sx={visuallyHidden}>
  701. {order === "desc"
  702. ? "sorted descending"
  703. : "sorted ascending"}
  704. </Box>
  705. ) : null}
  706. </TableSortLabel>
  707. )}
  708. </TableCell>
  709. ))}
  710. </TableRow>
  711. );
  712. }
  713. })}
  714. </TableHead>
  715. <TableBody>
  716. {rows?.map((row, index) => {
  717. const sel = selected.indexOf(index + startIndex);
  718. if (sel == 0) {
  719. Promise.resolve().then(
  720. () =>
  721. selectedRowRef.current?.scrollIntoView &&
  722. selectedRowRef.current.scrollIntoView({ block: "center" })
  723. );
  724. }
  725. return (
  726. <TableRow
  727. hover
  728. tabIndex={-1}
  729. key={`row${index}`}
  730. selected={sel > -1}
  731. ref={sel == 0 ? selectedRowRef : undefined}
  732. className={getClassName(row, props.rowClassName)}
  733. data-index={index}
  734. onClick={active && onAction ? onRowClick : undefined}
  735. >
  736. {colsOrder.map((col, idx) => {
  737. const rowSpan = idx < rowSpans.length && index < rowSpans[idx].length ? rowSpans[idx][index] : 1;
  738. return (
  739. <EditableCell
  740. key={`cell${index}${columns[col].dfid}`}
  741. className={getClassName(row, columns[col].className, col)}
  742. tableClassName={className}
  743. colDesc={columns[col]}
  744. value={row[col]}
  745. formattedVal={getFormatFn(row, columns[col].formatFn, col)}
  746. formatConfig={formatConfig}
  747. rowIndex={index}
  748. onValidation={
  749. active && !columns[col].notEditable && onEdit
  750. ? onCellValidation
  751. : undefined
  752. }
  753. onDeletion={
  754. active && (editable || partialEditable) && onDelete
  755. ? onRowDeletion
  756. : undefined
  757. }
  758. onSelection={active && onAction ? onRowSelection : undefined}
  759. nanValue={columns[col].nanValue || props.nanValue}
  760. tooltip={getTooltip(row, columns[col].tooltip, col)}
  761. comp={compRows && compRows[index] && compRows[index][col]}
  762. useCheckbox={useCheckbox}
  763. rowSpan={rowSpan}
  764. />
  765. );
  766. })}
  767. </TableRow>
  768. );
  769. })}
  770. {!rows &&
  771. loading &&
  772. Array.from(Array(30).keys(), (v, idx) => (
  773. <TableRow hover key={"rowSkel" + idx}>
  774. {colsOrder.map((col, cIdx) => (
  775. <TableCell key={"skel" + cIdx}>
  776. <Skeleton sx={skeletonSx} />
  777. </TableCell>
  778. ))}
  779. </TableRow>
  780. ))}
  781. </TableBody>
  782. </Table>
  783. </TableContainer>
  784. </Tooltip>
  785. {!showAll &&
  786. (loading ? (
  787. <Skeleton sx={loadingStyle}>
  788. <Typography>Loading...</Typography>
  789. </Skeleton>
  790. ) : (
  791. <TablePagination
  792. component="div"
  793. count={rowCount}
  794. page={startIndex / rowsPerPage}
  795. rowsPerPage={rowsPerPage}
  796. showFirstButton={true}
  797. showLastButton={true}
  798. rowsPerPageOptions={pso}
  799. onPageChange={handleChangePage}
  800. onRowsPerPageChange={handleChangeRowsPerPage}
  801. />
  802. ))}
  803. </Paper>
  804. {props.children}
  805. </Box>
  806. );
  807. };
  808. export default PaginatedTable;