Table.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import { FC } from 'react';
  2. import Table from '@mui/material/Table';
  3. import TableBody from '@mui/material/TableBody';
  4. import TableCell from '@mui/material/TableCell';
  5. import TableContainer from '@mui/material/TableContainer';
  6. import TableRow from '@mui/material/TableRow';
  7. import Checkbox from '@mui/material/Checkbox';
  8. import Box from '@mui/material/Box';
  9. import Button from '@mui/material/Button';
  10. import Typography from '@mui/material/Typography';
  11. import EnhancedTableHead from './TableHead';
  12. import EditableTableHead from './TableEditableHead';
  13. import ActionBar from './ActionBar';
  14. import LoadingTable from './LoadingTable';
  15. import CopyButton from '../advancedSearch/CopyButton';
  16. import type { Theme } from '@mui/material/styles';
  17. import type { TableType } from './Types';
  18. /**
  19. * Enhanced Table Component
  20. *
  21. * Usage example with spacer column:
  22. * <AttuGrid
  23. * colDefinitions={[
  24. * { id: 'name', label: 'Name', disablePadding: false },
  25. * { id: 'age', label: 'Age', disablePadding: false }
  26. * ]}
  27. * addSpacerColumn={true} // This will add a spacer column that absorbs remaining space
  28. * // ... other props
  29. * />
  30. *
  31. * The spacer column will:
  32. * - Have width: 'auto' and minWidth: 'auto'
  33. * - Display no content
  34. * - Naturally absorb remaining horizontal space without forcing table width
  35. * - Prevent unnecessary horizontal scrollbars
  36. */
  37. const EnhancedTable: FC<TableType> = props => {
  38. let {
  39. selected,
  40. onSelected,
  41. isSelected,
  42. onSelectedAll,
  43. rows = [],
  44. colDefinitions,
  45. primaryKey,
  46. // whether show checkbox in the first column
  47. // set true as default
  48. openCheckBox = true,
  49. disableSelect,
  50. noData,
  51. // whether change table row background color when mouse hover
  52. // set true as default
  53. showHoverStyle = true,
  54. isLoading,
  55. headEditable = false,
  56. // editable heads required param, contains heads components and its value
  57. editHeads = [],
  58. // if table cell max width not be passed, table row will use 300px as default
  59. tableCellMaxWidth = '300px',
  60. handleSort,
  61. order,
  62. orderBy,
  63. rowDecorator = () => ({}),
  64. rowHeight,
  65. // whether to add a spacer column to control space distribution
  66. addSpacerColumn = false,
  67. } = props;
  68. const hasData = rows && rows.length > 0;
  69. // Add spacer column definition if needed
  70. const finalColDefinitions = addSpacerColumn
  71. ? [
  72. ...colDefinitions,
  73. {
  74. id: '__spacer__',
  75. align: 'left' as const,
  76. disablePadding: false,
  77. label: '',
  78. getStyle: () => ({
  79. width: '66.7%',
  80. minWidth: 'auto',
  81. }),
  82. formatter: () => null,
  83. },
  84. ]
  85. : colDefinitions;
  86. return (
  87. <TableContainer
  88. sx={theme => ({
  89. width: '100%',
  90. flexGrow: 1,
  91. color: theme.palette.text.primary,
  92. backgroundColor: theme.palette.background.paper,
  93. })}
  94. >
  95. <Box height="100%">
  96. <Table
  97. stickyHeader
  98. sx={{
  99. minWidth: '100%',
  100. height: hasData ? 'auto' : 'fit-content',
  101. }}
  102. aria-labelledby="tableTitle"
  103. size="medium"
  104. aria-label="enhanced table"
  105. >
  106. {!headEditable ? (
  107. <EnhancedTableHead
  108. colDefinitions={finalColDefinitions}
  109. numSelected={selected.length}
  110. order={order}
  111. orderBy={orderBy}
  112. onSelectAllClick={onSelectedAll}
  113. handleSort={handleSort}
  114. rowCount={rows.length}
  115. openCheckBox={openCheckBox}
  116. disableSelect={disableSelect}
  117. />
  118. ) : (
  119. <EditableTableHead editHeads={editHeads} />
  120. )}
  121. {!isLoading ? (
  122. <TableBody>
  123. {hasData ? (
  124. rows.map((row, index) => {
  125. const isItemSelected = isSelected(row);
  126. const labelId = `enhanced-table-checkbox-${index}`;
  127. return (
  128. <TableRow
  129. hover={showHoverStyle}
  130. key={'row' + row[primaryKey] + index}
  131. onClick={event => onSelected(event, row)}
  132. aria-checked={isItemSelected}
  133. tabIndex={-1}
  134. selected={isItemSelected && !disableSelect}
  135. sx={
  136. [
  137. showHoverStyle && {
  138. '&:hover': {
  139. '& td': {
  140. background: 'inherit',
  141. },
  142. '& .hoverActionCell': {
  143. '& span': {
  144. opacity: 1,
  145. },
  146. },
  147. },
  148. },
  149. isItemSelected &&
  150. !disableSelect && {
  151. '& td': {
  152. background: 'inherit',
  153. },
  154. },
  155. rowDecorator(row),
  156. ].filter(Boolean) as any
  157. }
  158. >
  159. {openCheckBox && (
  160. <TableCell
  161. padding="checkbox"
  162. sx={theme => ({
  163. width: '38px',
  164. borderBottom: `1px solid ${theme.palette.divider}`,
  165. })}
  166. >
  167. <Checkbox
  168. checked={isItemSelected}
  169. color="primary"
  170. size="small"
  171. disabled={disableSelect}
  172. inputProps={{
  173. 'aria-labelledby': labelId,
  174. role: 'checkbox',
  175. }}
  176. />
  177. </TableCell>
  178. )}
  179. {finalColDefinitions.map((colDef, i) => {
  180. const { actionBarConfigs = [], needCopy = false } =
  181. colDef;
  182. const cellStyleFromDef = colDef.getStyle
  183. ? colDef.getStyle(row[colDef.id])
  184. : {};
  185. // Skip rendering for spacer column
  186. if (colDef.id === '__spacer__') {
  187. return (
  188. <TableCell
  189. key={colDef.id}
  190. sx={[
  191. (theme: Theme) => ({
  192. borderBottom: `1px solid ${theme.palette.divider}`,
  193. }),
  194. cellStyleFromDef,
  195. ]}
  196. >
  197. {/* Empty content for spacer column */}
  198. </TableCell>
  199. );
  200. }
  201. return colDef.showActionCell ? (
  202. <TableCell
  203. sx={
  204. [
  205. (theme: Theme) => ({
  206. borderBottom: `1px solid ${theme.palette.divider}`,
  207. '& p': {
  208. display: 'inline-block',
  209. verticalAlign: 'middle',
  210. overflow: 'hidden',
  211. textOverflow: 'ellipsis',
  212. whiteSpace: 'nowrap',
  213. maxWidth: tableCellMaxWidth,
  214. },
  215. }),
  216. (theme: Theme) => ({
  217. padding: theme.spacing(1, 1.5),
  218. }),
  219. colDef.isHoverAction && {
  220. transition: '0.2s all',
  221. padding: 0,
  222. width: '50px',
  223. '& span': {
  224. opacity: 0,
  225. },
  226. },
  227. cellStyleFromDef,
  228. ].filter(Boolean) as any
  229. }
  230. className={
  231. colDef.isHoverAction ? 'hoverActionCell' : ''
  232. } // Keep class for targeting in rowHoverStyles
  233. key={colDef.id}
  234. >
  235. <ActionBar
  236. configs={actionBarConfigs}
  237. isHoverType={colDef.isHoverAction}
  238. row={row}
  239. ></ActionBar>
  240. </TableCell>
  241. ) : (
  242. <TableCell
  243. key={'cell' + row[primaryKey] + i}
  244. align={colDef.align || 'left'}
  245. sx={
  246. [
  247. (theme: Theme) => ({
  248. overflow: 'hidden',
  249. borderBottom: `1px solid ${theme.palette.divider}`,
  250. '& p': {
  251. display: 'inline-block',
  252. verticalAlign: 'middle',
  253. overflow: 'hidden',
  254. textOverflow: 'ellipsis',
  255. whiteSpace: 'nowrap',
  256. maxWidth: tableCellMaxWidth,
  257. },
  258. }),
  259. (theme: Theme) => ({
  260. padding: theme.spacing(1),
  261. }),
  262. cellStyleFromDef,
  263. ].filter(Boolean) as any
  264. }
  265. >
  266. <Box
  267. sx={{
  268. display: 'flex',
  269. whiteSpace: 'nowrap',
  270. alignItems: 'center',
  271. minHeight: rowHeight - 16, // 16 is the padding of the table cell
  272. }}
  273. >
  274. {typeof row[colDef.id] !== 'undefined' && (
  275. <>
  276. {colDef.formatter ? (
  277. colDef.onClick ? (
  278. <Button
  279. color="primary"
  280. data-data={row[colDef.id]}
  281. data-index={index}
  282. size="small"
  283. onClick={e => {
  284. colDef.onClick &&
  285. colDef.onClick(e, row);
  286. }}
  287. >
  288. <Typography
  289. component="div"
  290. variant="body2"
  291. title={String(row[colDef.id])}
  292. >
  293. {colDef.formatter(
  294. row,
  295. row[colDef.id],
  296. i
  297. )}
  298. </Typography>
  299. </Button>
  300. ) : (
  301. <Typography
  302. component="div"
  303. title={String(row[colDef.id])}
  304. variant="body2"
  305. >
  306. {colDef.formatter(
  307. row,
  308. row[colDef.id],
  309. i
  310. )}
  311. </Typography>
  312. )
  313. ) : colDef.onClick ? (
  314. <Button
  315. color="primary"
  316. data-data={row[colDef.id]}
  317. data-index={index}
  318. size="small"
  319. onClick={e => {
  320. colDef.onClick &&
  321. colDef.onClick(e, row);
  322. }}
  323. >
  324. <Typography
  325. component="div"
  326. title={String(row[colDef.id])}
  327. variant="body2"
  328. >
  329. {row[colDef.id]}
  330. </Typography>
  331. </Button>
  332. ) : (
  333. <Typography
  334. component="div"
  335. title={String(row[colDef.id])}
  336. variant="body2"
  337. >
  338. {row[colDef.id]}
  339. </Typography>
  340. )}
  341. {needCopy && (
  342. <CopyButton
  343. copyValue={row[colDef.id]}
  344. size="small"
  345. sx={theme => ({
  346. '& svg': {
  347. fontSize: '13px',
  348. },
  349. marginLeft: theme.spacing(0.5),
  350. })}
  351. />
  352. )}
  353. </>
  354. )}
  355. </Box>
  356. </TableCell>
  357. );
  358. })}
  359. </TableRow>
  360. );
  361. })
  362. ) : (
  363. <TableRow>
  364. <TableCell
  365. sx={theme => ({
  366. paddingTop: theme.spacing(6),
  367. textAlign: 'center',
  368. letterSpacing: '0.5px',
  369. color: theme.palette.text.secondary,
  370. })}
  371. colSpan={
  372. openCheckBox
  373. ? finalColDefinitions.length + 1
  374. : finalColDefinitions.length
  375. }
  376. >
  377. {noData}
  378. </TableCell>
  379. </TableRow>
  380. )}
  381. </TableBody>
  382. ) : (
  383. <TableBody>
  384. <TableRow>
  385. <TableCell
  386. sx={theme => ({
  387. paddingTop: theme.spacing(6),
  388. textAlign: 'center',
  389. letterSpacing: '0.5px',
  390. color: theme.palette.text.secondary,
  391. })}
  392. colSpan={
  393. openCheckBox
  394. ? finalColDefinitions.length + 1
  395. : finalColDefinitions.length
  396. }
  397. >
  398. <LoadingTable />
  399. </TableCell>
  400. </TableRow>
  401. </TableBody>
  402. )}
  403. </Table>
  404. </Box>
  405. </TableContainer>
  406. );
  407. };
  408. export default EnhancedTable;