data-table.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. 'use client';
  2. import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
  3. import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
  4. import { Input } from '@/components/ui/input.js';
  5. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
  6. import {
  7. ColumnDef,
  8. ColumnFilter,
  9. ColumnFiltersState,
  10. flexRender,
  11. getCoreRowModel,
  12. getPaginationRowModel,
  13. PaginationState,
  14. SortingState,
  15. Table as TableType,
  16. useReactTable,
  17. VisibilityState,
  18. } from '@tanstack/react-table';
  19. import { TableOptions } from '@tanstack/table-core';
  20. import React, { Suspense, useEffect } from 'react';
  21. import { AddFilterMenu } from './add-filter-menu.js';
  22. import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
  23. import { DataTableFilterBadge } from './data-table-filter-badge.js';
  24. import { useChannel } from '@/hooks/use-channel.js';
  25. export interface FacetedFilter {
  26. title: string;
  27. icon?: React.ComponentType<{ className?: string }>;
  28. optionsFn?: () => Promise<DataTableFacetedFilterOption[]>;
  29. options?: DataTableFacetedFilterOption[];
  30. }
  31. interface DataTableProps<TData, TValue> {
  32. columns: ColumnDef<TData, TValue>[];
  33. data: TData[];
  34. totalItems: number;
  35. page?: number;
  36. itemsPerPage?: number;
  37. sorting?: SortingState;
  38. columnFilters?: ColumnFiltersState;
  39. onPageChange?: (table: TableType<TData>, page: number, itemsPerPage: number) => void;
  40. onSortChange?: (table: TableType<TData>, sorting: SortingState) => void;
  41. onFilterChange?: (table: TableType<TData>, columnFilters: ColumnFilter[]) => void;
  42. onColumnVisibilityChange?: (table: TableType<TData>, columnVisibility: VisibilityState) => void;
  43. onSearchTermChange?: (searchTerm: string) => void;
  44. defaultColumnVisibility?: VisibilityState;
  45. facetedFilters?: { [key: string]: FacetedFilter | undefined };
  46. disableViewOptions?: boolean;
  47. /**
  48. * This property allows full control over _all_ features of TanStack Table
  49. * when needed.
  50. */
  51. setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
  52. }
  53. export function DataTable<TData, TValue>({
  54. columns,
  55. data,
  56. totalItems,
  57. page,
  58. itemsPerPage,
  59. sorting: sortingInitialState,
  60. columnFilters: filtersInitialState,
  61. onPageChange,
  62. onSortChange,
  63. onFilterChange,
  64. onSearchTermChange,
  65. onColumnVisibilityChange,
  66. defaultColumnVisibility,
  67. facetedFilters,
  68. disableViewOptions,
  69. setTableOptions,
  70. }: DataTableProps<TData, TValue>) {
  71. const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
  72. const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
  73. const { activeChannel } = useChannel();
  74. const [pagination, setPagination] = React.useState<PaginationState>({
  75. pageIndex: (page ?? 1) - 1,
  76. pageSize: itemsPerPage ?? 10,
  77. });
  78. const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
  79. defaultColumnVisibility ?? {},
  80. );
  81. let tableOptions: TableOptions<TData> = {
  82. data,
  83. columns,
  84. getCoreRowModel: getCoreRowModel(),
  85. getPaginationRowModel: getPaginationRowModel(),
  86. manualPagination: true,
  87. manualSorting: true,
  88. manualFiltering: true,
  89. rowCount: totalItems,
  90. onPaginationChange: setPagination,
  91. onSortingChange: setSorting,
  92. onColumnVisibilityChange: setColumnVisibility,
  93. onColumnFiltersChange: setColumnFilters,
  94. state: {
  95. pagination,
  96. sorting,
  97. columnVisibility,
  98. columnFilters,
  99. },
  100. };
  101. if (typeof setTableOptions === 'function') {
  102. tableOptions = setTableOptions(tableOptions);
  103. }
  104. const table = useReactTable(tableOptions);
  105. useEffect(() => {
  106. onPageChange?.(table, pagination.pageIndex + 1, pagination.pageSize);
  107. }, [pagination]);
  108. useEffect(() => {
  109. onSortChange?.(table, sorting);
  110. }, [sorting]);
  111. useEffect(() => {
  112. onFilterChange?.(table, columnFilters);
  113. }, [columnFilters]);
  114. useEffect(() => {
  115. onColumnVisibilityChange?.(table, columnVisibility);
  116. }, [columnVisibility]);
  117. return (
  118. <>
  119. <div className="flex justify-between items-start">
  120. <div className="flex flex-col space-y-2">
  121. <div className="flex items-center justify-start gap-2">
  122. {onSearchTermChange && (
  123. <div className="flex items-center">
  124. <Input
  125. placeholder="Filter..."
  126. onChange={event => onSearchTermChange(event.target.value)}
  127. className="max-w-sm w-md"
  128. />
  129. </div>
  130. )}
  131. <Suspense>
  132. {Object.entries(facetedFilters ?? {}).map(([key, filter]) => (
  133. <DataTableFacetedFilter
  134. key={key}
  135. column={table.getColumn(key)}
  136. title={filter?.title}
  137. options={filter?.options}
  138. optionsFn={filter?.optionsFn}
  139. />
  140. ))}
  141. </Suspense>
  142. <AddFilterMenu columns={table.getAllColumns()} />
  143. </div>
  144. <div className="flex gap-1">
  145. {columnFilters
  146. .filter(f => !facetedFilters?.[f.id])
  147. .map(f => {
  148. const column = table.getColumn(f.id);
  149. const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
  150. return <DataTableFilterBadge
  151. key={f.id}
  152. filter={f}
  153. currencyCode={currency}
  154. dataType={(column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'}
  155. onRemove={() => setColumnFilters(old => old.filter(x => x.id !== f.id))} />;
  156. })}
  157. </div>
  158. </div>
  159. {!disableViewOptions && <DataTableViewOptions table={table} />}
  160. </div>
  161. <div className="rounded-md border my-2">
  162. <Table>
  163. <TableHeader>
  164. {table.getHeaderGroups().map(headerGroup => (
  165. <TableRow key={headerGroup.id}>
  166. {headerGroup.headers.map(header => {
  167. return (
  168. <TableHead key={header.id}>
  169. {header.isPlaceholder
  170. ? null
  171. : flexRender(
  172. header.column.columnDef.header,
  173. header.getContext(),
  174. )}
  175. </TableHead>
  176. );
  177. })}
  178. </TableRow>
  179. ))}
  180. </TableHeader>
  181. <TableBody>
  182. {table.getRowModel().rows?.length ? (
  183. table.getRowModel().rows.map(row => (
  184. <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
  185. {row.getVisibleCells().map(cell => (
  186. <TableCell key={cell.id}>
  187. {flexRender(cell.column.columnDef.cell, cell.getContext())}
  188. </TableCell>
  189. ))}
  190. </TableRow>
  191. ))
  192. ) : (
  193. <TableRow>
  194. <TableCell colSpan={columns.length} className="h-24 text-center">
  195. No results.
  196. </TableCell>
  197. </TableRow>
  198. )}
  199. </TableBody>
  200. </Table>
  201. </div>
  202. <DataTablePagination table={table} />
  203. </>
  204. );
  205. }