Table.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import React from 'react';
  2. import {
  3. flexRender,
  4. getCoreRowModel,
  5. getExpandedRowModel,
  6. getSortedRowModel,
  7. useReactTable,
  8. getPaginationRowModel,
  9. } from '@tanstack/react-table';
  10. import type {
  11. Row,
  12. SortingState,
  13. OnChangeFn,
  14. PaginationState,
  15. ColumnDef,
  16. } from '@tanstack/react-table';
  17. import { useSearchParams } from 'react-router-dom';
  18. import { PER_PAGE } from 'lib/constants';
  19. import { Button } from 'components/common/Button/Button';
  20. import Input from 'components/common/Input/Input';
  21. import * as S from './Table.styled';
  22. import updateSortingState from './utils/updateSortingState';
  23. import updatePaginationState from './utils/updatePaginationState';
  24. import ExpanderCell from './ExpanderCell';
  25. import SelectRowCell from './SelectRowCell';
  26. import SelectRowHeader from './SelectRowHeader';
  27. export interface TableProps<TData> {
  28. data: TData[];
  29. pageCount?: number;
  30. columns: ColumnDef<TData>[];
  31. // Server-side processing: sorting, pagination
  32. serverSideProcessing?: boolean;
  33. // Expandeble rows
  34. getRowCanExpand?: (row: Row<TData>) => boolean; // Enables the ability to expand row. Use `() => true` when want to expand all rows.
  35. renderSubComponent?: React.FC<{ row: Row<TData> }>; // Component to render expanded row.
  36. // Selectable rows
  37. enableRowSelection?: boolean | ((row: Row<TData>) => boolean); // Enables the ability to select row.
  38. batchActionsBar?: React.FC<{ rows: Row<TData>[]; resetRowSelection(): void }>; // Component to render batch actions bar for slected rows
  39. // Sorting.
  40. enableSorting?: boolean; // Enables sorting for table.
  41. // Placeholder for empty table
  42. emptyMessage?: React.ReactNode;
  43. disabled?: boolean;
  44. // Handles row click. Can not be combined with `enableRowSelection` && expandable rows.
  45. onRowClick?: (row: Row<TData>) => void;
  46. }
  47. type UpdaterFn<T> = (previousState: T) => T;
  48. const getPaginationFromSearchParams = (searchParams: URLSearchParams) => {
  49. const page = searchParams.get('page');
  50. const perPage = searchParams.get('perPage');
  51. const pageIndex = page ? Number(page) - 1 : 0;
  52. return {
  53. pageIndex,
  54. pageSize: Number(perPage || PER_PAGE),
  55. };
  56. };
  57. const getSortingFromSearchParams = (searchParams: URLSearchParams) => {
  58. const sortBy = searchParams.get('sortBy');
  59. const sortDirection = searchParams.get('sortDirection');
  60. if (!sortBy) return [];
  61. return [{ id: sortBy, desc: sortDirection === 'desc' }];
  62. };
  63. /**
  64. * Table component that uses the react-table library to render a table.
  65. * https://tanstack.com/table/v8
  66. *
  67. * The most important props are:
  68. * - `data`: the data to render in the table
  69. * - `columns`: ColumnsDef. You can finde more info about it on https://tanstack.com/table/v8/docs/guide/column-defs
  70. * - `emptyMessage`: the message to show when there is no data to render
  71. *
  72. * Usecases:
  73. * 1. Sortable table
  74. * - set `enableSorting` property of component to true. It will enable sorting for all columns.
  75. * If you want to disable sorting for some particular columns you can pass
  76. * `enableSorting = false` to the column def.
  77. * - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection`
  78. * search param to set default sortings.
  79. * - use `id` property of the column def to set the sortBy for server side sorting.
  80. *
  81. * 2. Pagination
  82. * - pagination enabled by default.
  83. * - use `perPage` search param to manage default page size.
  84. * - use `page` search param to manage default page index.
  85. * - use `pageCount` prop to set the total number of pages only in case of server side processing.
  86. *
  87. * 3. Expandable rows
  88. * - use `getRowCanExpand` prop to set a function that returns true if the row can be expanded.
  89. * - use `renderSubComponent` prop to provide a sub component for each expanded row.
  90. *
  91. * 4. Row selection
  92. * - use `enableRowSelection` prop to enable row selection. This prop can be a boolean or
  93. * a function that returns true if the particular row can be selected.
  94. * - use `batchActionsBar` prop to provide a component that will be rendered at the top of the table
  95. * when row selection is enabled.
  96. *
  97. * 5. Server side processing:
  98. * - set `serverSideProcessing` to true
  99. * - set `pageCount` to the total number of pages
  100. * - use URLSearchParams to get the pagination and sorting state from the url for your server side processing.
  101. */
  102. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  103. const Table: React.FC<TableProps<any>> = ({
  104. data,
  105. pageCount,
  106. columns,
  107. getRowCanExpand,
  108. renderSubComponent: SubComponent,
  109. serverSideProcessing = false,
  110. enableSorting = false,
  111. enableRowSelection = false,
  112. batchActionsBar: BatchActionsBar,
  113. emptyMessage,
  114. disabled,
  115. onRowClick,
  116. }) => {
  117. const [searchParams, setSearchParams] = useSearchParams();
  118. const [rowSelection, setRowSelection] = React.useState({});
  119. const onSortingChange = React.useCallback(
  120. (updater: UpdaterFn<SortingState>) => {
  121. const newState = updateSortingState(updater, searchParams);
  122. setSearchParams(searchParams);
  123. return newState;
  124. },
  125. [searchParams]
  126. );
  127. const onPaginationChange = React.useCallback(
  128. (updater: UpdaterFn<PaginationState>) => {
  129. const newState = updatePaginationState(updater, searchParams);
  130. setSearchParams(searchParams);
  131. setRowSelection({});
  132. return newState;
  133. },
  134. [searchParams]
  135. );
  136. const table = useReactTable({
  137. data,
  138. pageCount,
  139. columns,
  140. state: {
  141. sorting: getSortingFromSearchParams(searchParams),
  142. pagination: getPaginationFromSearchParams(searchParams),
  143. rowSelection,
  144. },
  145. getRowId: (originalRow, index) => {
  146. return originalRow.name ? originalRow.name : `${index}`;
  147. },
  148. onSortingChange: onSortingChange as OnChangeFn<SortingState>,
  149. onPaginationChange: onPaginationChange as OnChangeFn<PaginationState>,
  150. onRowSelectionChange: setRowSelection,
  151. getRowCanExpand,
  152. getCoreRowModel: getCoreRowModel(),
  153. getExpandedRowModel: getExpandedRowModel(),
  154. getSortedRowModel: getSortedRowModel(),
  155. getPaginationRowModel: getPaginationRowModel(),
  156. manualSorting: serverSideProcessing,
  157. manualPagination: serverSideProcessing,
  158. enableSorting,
  159. autoResetPageIndex: false,
  160. enableRowSelection,
  161. });
  162. const handleRowClick = (row: Row<typeof data>) => (e: React.MouseEvent) => {
  163. // If row selection is enabled do not handle row click.
  164. if (enableRowSelection) return undefined;
  165. // If row can be expanded do not handle row click.
  166. if (row.getCanExpand()) {
  167. e.stopPropagation();
  168. return row.toggleExpanded();
  169. }
  170. if (onRowClick) {
  171. e.stopPropagation();
  172. return onRowClick(row);
  173. }
  174. return undefined;
  175. };
  176. return (
  177. <>
  178. {BatchActionsBar && (
  179. <S.TableActionsBar>
  180. <BatchActionsBar
  181. rows={table.getSelectedRowModel().flatRows}
  182. resetRowSelection={table.resetRowSelection}
  183. />
  184. </S.TableActionsBar>
  185. )}
  186. <S.TableWrapper $disabled={!!disabled}>
  187. <S.Table>
  188. <thead>
  189. {table.getHeaderGroups().map((headerGroup) => (
  190. <tr key={headerGroup.id}>
  191. {!!enableRowSelection && (
  192. <S.Th key={`${headerGroup.id}-select`}>
  193. {flexRender(
  194. SelectRowHeader,
  195. headerGroup.headers[0].getContext()
  196. )}
  197. </S.Th>
  198. )}
  199. {table.getCanSomeRowsExpand() && (
  200. <S.Th expander key={`${headerGroup.id}-expander`} />
  201. )}
  202. {headerGroup.headers.map((header) => (
  203. <S.Th
  204. key={header.id}
  205. colSpan={header.colSpan}
  206. sortable={header.column.getCanSort()}
  207. sortOrder={header.column.getIsSorted()}
  208. onClick={header.column.getToggleSortingHandler()}
  209. >
  210. <div>
  211. {flexRender(
  212. header.column.columnDef.header,
  213. header.getContext()
  214. )}
  215. </div>
  216. </S.Th>
  217. ))}
  218. </tr>
  219. ))}
  220. </thead>
  221. <tbody>
  222. {table.getRowModel().rows.map((row) => (
  223. <React.Fragment key={row.id}>
  224. <S.Row
  225. expanded={row.getIsExpanded()}
  226. onClick={handleRowClick(row)}
  227. clickable={
  228. !enableRowSelection &&
  229. (row.getCanExpand() || onRowClick !== undefined)
  230. }
  231. >
  232. {!!enableRowSelection && (
  233. <td key={`${row.id}-select`} style={{ width: '1px' }}>
  234. {flexRender(
  235. SelectRowCell,
  236. row.getVisibleCells()[0].getContext()
  237. )}
  238. </td>
  239. )}
  240. {table.getCanSomeRowsExpand() && (
  241. <td key={`${row.id}-expander`} style={{ width: '1px' }}>
  242. {flexRender(
  243. ExpanderCell,
  244. row.getVisibleCells()[0].getContext()
  245. )}
  246. </td>
  247. )}
  248. {row
  249. .getVisibleCells()
  250. .map(({ id, getContext, column: { columnDef } }) => (
  251. <td key={id} style={columnDef.meta}>
  252. {flexRender(columnDef.cell, getContext())}
  253. </td>
  254. ))}
  255. </S.Row>
  256. {row.getIsExpanded() && SubComponent && (
  257. <S.Row expanded>
  258. <td colSpan={row.getVisibleCells().length + 2}>
  259. <S.ExpandedRowInfo>
  260. <SubComponent row={row} />
  261. </S.ExpandedRowInfo>
  262. </td>
  263. </S.Row>
  264. )}
  265. </React.Fragment>
  266. ))}
  267. {table.getRowModel().rows.length === 0 && (
  268. <S.Row>
  269. <S.EmptyTableMessageCell colSpan={100}>
  270. {emptyMessage || 'No rows found'}
  271. </S.EmptyTableMessageCell>
  272. </S.Row>
  273. )}
  274. </tbody>
  275. </S.Table>
  276. </S.TableWrapper>
  277. {table.getPageCount() > 1 && (
  278. <S.Pagination>
  279. <S.Pages>
  280. <Button
  281. buttonType="secondary"
  282. buttonSize="M"
  283. onClick={() => table.setPageIndex(0)}
  284. disabled={!table.getCanPreviousPage()}
  285. >
  286. </Button>
  287. <Button
  288. buttonType="secondary"
  289. buttonSize="M"
  290. onClick={() => table.previousPage()}
  291. disabled={!table.getCanPreviousPage()}
  292. >
  293. ← Previous
  294. </Button>
  295. <Button
  296. buttonType="secondary"
  297. buttonSize="M"
  298. onClick={() => table.nextPage()}
  299. disabled={!table.getCanNextPage()}
  300. >
  301. Next →
  302. </Button>
  303. <Button
  304. buttonType="secondary"
  305. buttonSize="M"
  306. onClick={() => table.setPageIndex(table.getPageCount() - 1)}
  307. disabled={!table.getCanNextPage()}
  308. >
  309. </Button>
  310. <S.GoToPage>
  311. <span>Go to page:</span>
  312. <Input
  313. type="number"
  314. positiveOnly
  315. defaultValue={table.getState().pagination.pageIndex + 1}
  316. inputSize="M"
  317. max={table.getPageCount()}
  318. min={1}
  319. onChange={({ target: { value } }) => {
  320. const index = value ? Number(value) - 1 : 0;
  321. table.setPageIndex(index);
  322. }}
  323. />
  324. </S.GoToPage>
  325. </S.Pages>
  326. <S.PageInfo>
  327. <span>
  328. Page {table.getState().pagination.pageIndex + 1} of{' '}
  329. {table.getPageCount()}{' '}
  330. </span>
  331. </S.PageInfo>
  332. </S.Pagination>
  333. )}
  334. </>
  335. );
  336. };
  337. export default Table;