Table.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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, useLocation } 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 location = useLocation();
  119. const [rowSelection, setRowSelection] = React.useState({});
  120. const onSortingChange = React.useCallback(
  121. (updater: UpdaterFn<SortingState>) => {
  122. const newState = updateSortingState(updater, searchParams);
  123. setSearchParams(searchParams);
  124. return newState;
  125. },
  126. [searchParams, location]
  127. );
  128. const onPaginationChange = React.useCallback(
  129. (updater: UpdaterFn<PaginationState>) => {
  130. const newState = updatePaginationState(updater, searchParams);
  131. setSearchParams(searchParams);
  132. setRowSelection({});
  133. return newState;
  134. },
  135. [searchParams, location]
  136. );
  137. const table = useReactTable({
  138. data,
  139. pageCount,
  140. columns,
  141. state: {
  142. sorting: getSortingFromSearchParams(searchParams),
  143. pagination: getPaginationFromSearchParams(searchParams),
  144. rowSelection,
  145. },
  146. getRowId: (originalRow, index) => {
  147. return originalRow.name ? originalRow.name : `${index}`;
  148. },
  149. onSortingChange: onSortingChange as OnChangeFn<SortingState>,
  150. onPaginationChange: onPaginationChange as OnChangeFn<PaginationState>,
  151. onRowSelectionChange: setRowSelection,
  152. getRowCanExpand,
  153. getCoreRowModel: getCoreRowModel(),
  154. getExpandedRowModel: getExpandedRowModel(),
  155. getSortedRowModel: getSortedRowModel(),
  156. getPaginationRowModel: getPaginationRowModel(),
  157. manualSorting: serverSideProcessing,
  158. manualPagination: serverSideProcessing,
  159. enableSorting,
  160. autoResetPageIndex: false,
  161. enableRowSelection,
  162. });
  163. const handleRowClick = (row: Row<typeof data>) => (e: React.MouseEvent) => {
  164. // If row selection is enabled do not handle row click.
  165. if (enableRowSelection) return undefined;
  166. // If row can be expanded do not handle row click.
  167. if (row.getCanExpand()) {
  168. e.stopPropagation();
  169. return row.toggleExpanded();
  170. }
  171. if (onRowClick) {
  172. e.stopPropagation();
  173. return onRowClick(row);
  174. }
  175. return undefined;
  176. };
  177. return (
  178. <>
  179. {BatchActionsBar && (
  180. <S.TableActionsBar>
  181. <BatchActionsBar
  182. rows={table.getSelectedRowModel().flatRows}
  183. resetRowSelection={table.resetRowSelection}
  184. />
  185. </S.TableActionsBar>
  186. )}
  187. <S.TableWrapper $disabled={!!disabled}>
  188. <S.Table>
  189. <thead>
  190. {table.getHeaderGroups().map((headerGroup) => (
  191. <tr key={headerGroup.id}>
  192. {!!enableRowSelection && (
  193. <S.Th key={`${headerGroup.id}-select`}>
  194. {flexRender(
  195. SelectRowHeader,
  196. headerGroup.headers[0].getContext()
  197. )}
  198. </S.Th>
  199. )}
  200. {table.getCanSomeRowsExpand() && (
  201. <S.Th expander key={`${headerGroup.id}-expander`} />
  202. )}
  203. {headerGroup.headers.map((header) => (
  204. <S.Th
  205. key={header.id}
  206. colSpan={header.colSpan}
  207. sortable={header.column.getCanSort()}
  208. sortOrder={header.column.getIsSorted()}
  209. onClick={header.column.getToggleSortingHandler()}
  210. >
  211. <div>
  212. {flexRender(
  213. header.column.columnDef.header,
  214. header.getContext()
  215. )}
  216. </div>
  217. </S.Th>
  218. ))}
  219. </tr>
  220. ))}
  221. </thead>
  222. <tbody>
  223. {table.getRowModel().rows.map((row) => (
  224. <React.Fragment key={row.id}>
  225. <S.Row
  226. expanded={row.getIsExpanded()}
  227. onClick={handleRowClick(row)}
  228. clickable={
  229. !enableRowSelection &&
  230. (row.getCanExpand() || onRowClick !== undefined)
  231. }
  232. >
  233. {!!enableRowSelection && (
  234. <td key={`${row.id}-select`} style={{ width: '1px' }}>
  235. {flexRender(
  236. SelectRowCell,
  237. row.getVisibleCells()[0].getContext()
  238. )}
  239. </td>
  240. )}
  241. {table.getCanSomeRowsExpand() && (
  242. <td key={`${row.id}-expander`} style={{ width: '1px' }}>
  243. {flexRender(
  244. ExpanderCell,
  245. row.getVisibleCells()[0].getContext()
  246. )}
  247. </td>
  248. )}
  249. {row
  250. .getVisibleCells()
  251. .map(({ id, getContext, column: { columnDef } }) => (
  252. <td key={id} style={columnDef.meta}>
  253. {flexRender(columnDef.cell, getContext())}
  254. </td>
  255. ))}
  256. </S.Row>
  257. {row.getIsExpanded() && SubComponent && (
  258. <S.Row expanded>
  259. <td colSpan={row.getVisibleCells().length + 2}>
  260. <S.ExpandedRowInfo>
  261. <SubComponent row={row} />
  262. </S.ExpandedRowInfo>
  263. </td>
  264. </S.Row>
  265. )}
  266. </React.Fragment>
  267. ))}
  268. {table.getRowModel().rows.length === 0 && (
  269. <S.Row>
  270. <S.EmptyTableMessageCell colSpan={100}>
  271. {emptyMessage || 'No rows found'}
  272. </S.EmptyTableMessageCell>
  273. </S.Row>
  274. )}
  275. </tbody>
  276. </S.Table>
  277. </S.TableWrapper>
  278. {table.getPageCount() > 1 && (
  279. <S.Pagination>
  280. <S.Pages>
  281. <Button
  282. buttonType="secondary"
  283. buttonSize="M"
  284. onClick={() => table.setPageIndex(0)}
  285. disabled={!table.getCanPreviousPage()}
  286. >
  287. </Button>
  288. <Button
  289. buttonType="secondary"
  290. buttonSize="M"
  291. onClick={() => table.previousPage()}
  292. disabled={!table.getCanPreviousPage()}
  293. >
  294. ← Previous
  295. </Button>
  296. <Button
  297. buttonType="secondary"
  298. buttonSize="M"
  299. onClick={() => table.nextPage()}
  300. disabled={!table.getCanNextPage()}
  301. >
  302. Next →
  303. </Button>
  304. <Button
  305. buttonType="secondary"
  306. buttonSize="M"
  307. onClick={() => table.setPageIndex(table.getPageCount() - 1)}
  308. disabled={!table.getCanNextPage()}
  309. >
  310. </Button>
  311. <S.GoToPage>
  312. <span>Go to page:</span>
  313. <Input
  314. type="number"
  315. positiveOnly
  316. defaultValue={table.getState().pagination.pageIndex + 1}
  317. inputSize="M"
  318. max={table.getPageCount()}
  319. min={1}
  320. onChange={({ target: { value } }) => {
  321. const index = value ? Number(value) - 1 : 0;
  322. table.setPageIndex(index);
  323. }}
  324. />
  325. </S.GoToPage>
  326. </S.Pages>
  327. <S.PageInfo>
  328. <span>
  329. Page {table.getState().pagination.pageIndex + 1} of{' '}
  330. {table.getPageCount()}{' '}
  331. </span>
  332. </S.PageInfo>
  333. </S.Pagination>
  334. )}
  335. </>
  336. );
  337. };
  338. export default Table;