diff --git a/src/components/DataTable/DataTable.tsx b/src/components/DataTable/DataTable.tsx new file mode 100644 index 0000000..68e5b0e --- /dev/null +++ b/src/components/DataTable/DataTable.tsx @@ -0,0 +1,143 @@ +import { + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import React, { useEffect } from "react"; + +import { useNavigate } from "react-router"; +import { TableContext } from "./TableContext"; +import { serializeQuery } from "#/lib/serializeQuery"; + +export function formatRequestParams(originalObj) { + return { + ...originalObj, + sorting: originalObj?.sorting?.length > 0 ? originalObj.sorting[0] : null, + columnFilters: originalObj.columnFilters.reduce((acc, filter) => { + acc[filter.id] = filter.value; + return acc; + }, {}), + }; +} + +const defaultFilterFn = (row, id, filterValue) => + row.getValue(id).includes(filterValue); + +const buildColumns = (columnsConfig) => { + if (!columnsConfig) return []; + + return columnsConfig.map((columnConfig) => ({ + ...columnConfig, + filterFn: columnConfig.filterable ? defaultFilterFn : null, + })); +}; + +export function DataTable({ + children, + data, + error, + tableState, + setTableState, + buildTableColumns = buildColumns, + setQueryToParams, + setSelectedData, +}) { + const { pagination, rowSelection, columnVisibility, columnFilters, sorting } = + tableState; + + const { + setPagination, + setRowSelection, + setColumnVisibility, + setColumnFilters, + setSorting, + } = setTableState; + + if (error) { + return
Error: {error.message}
; + } + + // @ts-ignore + const columns = buildTableColumns(data?.columns, data?.filters); + const filters = data?.filters; + const searchKey = data?.search?.key; + + const hiddenColumns = columns + .filter((c) => c.hide) + .map((c) => ({ [c.accessorKey]: false })) + .reduce((acc, obj) => Object.assign(acc, obj), {}); + + const table = useReactTable({ + data: data?.data ?? [], + pageCount: Math.ceil((data?.total ?? 0) / pagination.pageSize), + columns, + state: { + sorting, + rowSelection, + columnFilters, + pagination, + columnVisibility: { + ...columnVisibility, + ...hiddenColumns, + }, + }, + onPaginationChange: setPagination, + manualPagination: true, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + React.useEffect(() => { + if (setSelectedData) + setSelectedData( + table.getSelectedRowModel().flatRows.map((row) => row.original) + ); + }, [rowSelection, table, setSelectedData]); + + const navigate = useNavigate(); + useEffect(() => { + if (!setQueryToParams) return; + + const formattedParams = formatRequestParams({ + columnFilters: tableState.columnFilters, + sorting: tableState.sorting, + pageIndex: tableState.pagination.pageIndex, + pageSize: tableState.pagination.pageSize, + }); + + const params = serializeQuery(formattedParams); + navigate(`?${params}`, { replace: true }); + }, [ + tableState.pagination.pageIndex, + tableState.pagination.pageSize, + tableState.sorting, + tableState.columnFilters, + navigate, + ]); + + // eslint-disable-next-line react/jsx-no-constructed-context-values + const contextValue = { + data, + filters, + error, + tableState, + setTableState, + table, + searchKey, + }; + + return ( + + {children} + + ); +} diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx index 1cf1a06..e4c4972 100644 --- a/src/components/DataTable/DataTablePagination.tsx +++ b/src/components/DataTable/DataTablePagination.tsx @@ -7,8 +7,12 @@ import { } from "@radix-ui/react-icons"; import { useTranslation, Trans } from "react-i18next"; import { Button, Select } from "#/components/ui"; +import { useTableContext } from "./TableContext"; + +export function DataTablePagination() { + // @ts-expect-error TS(2339) FIXME: Property 'table' does not exist on type '{}'. + const { table } = useTableContext(); -export function DataTablePagination({ table }) { const { t } = useTranslation(); const currentPage = Number(table.getState().pagination.pageIndex) + 1; const pageCount = table.getPageCount() || 1; diff --git a/src/components/DataTable/DataTableToolbar.tsx b/src/components/DataTable/DataTableToolbar.tsx index a5c58f5..ce30a51 100644 --- a/src/components/DataTable/DataTableToolbar.tsx +++ b/src/components/DataTable/DataTableToolbar.tsx @@ -6,13 +6,18 @@ import { Button, Input } from "#/components/ui"; import { DataTableFacetedFilter } from "./DataTableFacetedFilter"; import { DataTableViewOptions } from "./DataTableViewOptions"; +import { useTableContext } from "./TableContext"; export function DataTableToolbar({ - table, - filters, action, + showViewOptions = true, searchKey = "name", }) { + // @ts-expect-error TS(2339) FIXME: Property 'table' does not exist on type '{}'. + const { table, filters, searchKey: TableSeachKey } = useTableContext(); + + const search = TableSeachKey || searchKey; + if (!table || !filters) { return null; } @@ -47,9 +52,9 @@ export function DataTableToolbar({
- table.getColumn(searchKey)?.setFilterValue(event.target.value) + table.getColumn(search)?.setFilterValue(event.target.value) } className="h-8 w-[150px] lg:w-[250px] dark:border-2" /> @@ -79,7 +84,7 @@ export function DataTableToolbar({
{action} - + {showViewOptions && }
); diff --git a/src/components/DataTable/DataTableViewOptions.tsx b/src/components/DataTable/DataTableViewOptions.tsx index 5c5e4f2..3acb2fa 100644 --- a/src/components/DataTable/DataTableViewOptions.tsx +++ b/src/components/DataTable/DataTableViewOptions.tsx @@ -10,8 +10,12 @@ import { DropdownMenuLabel, DropdownMenuSeparator, } from "#/components/ui"; +import { useTableContext } from "./TableContext"; + +export function DataTableViewOptions() { + // @ts-expect-error TS(2339) FIXME: Property 'table' does not exist on type '{}'. + const { table } = useTableContext(); -export function DataTableViewOptions({ table }) { return ( diff --git a/src/components/DataTable/SWRDataTable/SWRDataTableBody.tsx b/src/components/DataTable/SWRDataTable/SWRDataTableBody.tsx new file mode 100644 index 0000000..375bb98 --- /dev/null +++ b/src/components/DataTable/SWRDataTable/SWRDataTableBody.tsx @@ -0,0 +1,45 @@ +/* eslint-disable no-restricted-syntax */ +import React from "react"; +import { flexRender } from "@tanstack/react-table"; + +import { Trans } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { TableBody, TableCell, TableRow } from "#/components/ui"; + +import { useTableContext } from "../TableContext"; + +export function SWRDataTableBody({ hasDetails }) { + // @ts-expect-error TS(2339) FIXME: Property 'table' does not exist on type '{}'. + const { table } = useTableContext(); + + const navigate = useNavigate(); + return ( + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + if (hasDetails) { + navigate(`${row.original.id}`); + } + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results found. + + + )} + + ); +} diff --git a/src/components/DataTable/SWRDataTable/SWRDataTableHeader.tsx b/src/components/DataTable/SWRDataTable/SWRDataTableHeader.tsx new file mode 100644 index 0000000..40b823a --- /dev/null +++ b/src/components/DataTable/SWRDataTable/SWRDataTableHeader.tsx @@ -0,0 +1,31 @@ +/* eslint-disable no-restricted-syntax */ +import React from "react"; +import { flexRender } from "@tanstack/react-table"; + +import { TableHead, TableHeader, TableRow } from "#/components/ui"; + +import { useTableContext } from "../TableContext"; + +export function SWRDataTableHeader() { + // @ts-expect-error TS(2339) FIXME: Property 'table' does not exist on type '{}'. + const { table } = useTableContext(); + + return ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + ); +} diff --git a/src/components/DataTable/SWRDataTable/index.tsx b/src/components/DataTable/SWRDataTable/index.tsx index 71bf36f..f249fca 100644 --- a/src/components/DataTable/SWRDataTable/index.tsx +++ b/src/components/DataTable/SWRDataTable/index.tsx @@ -1,49 +1,23 @@ /* eslint-disable no-restricted-syntax */ -import React, { useEffect } from "react"; -import { - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import useSWR from "swr"; +import React from "react"; import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; import { Trans } from "react-i18next"; -import { - Badge, - Checkbox, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "#/components/ui"; +import { useSearchParams } from "react-router-dom"; +import { Badge, Checkbox, Table } from "#/components/ui"; import { SectionTitle } from "#/components/SectionTitle"; import { formatDate, formatDateTime } from "#/lib/formatDate"; -import { serializeQuery, deserializeQuery } from "#/lib/serializeQuery"; -import { useTableState } from "./useTableState"; import { Link } from "#/components/Link"; import { DataTableColumnHeader } from "#/components/DataTable/DataTableColumnHeader"; import { DataTablePagination } from "#/components/DataTable/DataTablePagination"; import { DataTableRowActions } from "#/components/DataTable/DataTableRowActions"; import { DataTableToolbar } from "#/components/DataTable/DataTableToolbar"; - -export function formatRequestParams(originalObj) { - return { - ...originalObj, - sorting: originalObj?.sorting?.length > 0 ? originalObj.sorting[0] : null, - columnFilters: originalObj.columnFilters.reduce((acc, filter) => { - acc[filter.id] = filter.value; - return acc; - }, {}), - }; -} +import { DataTable } from "../DataTable"; +import { useSWRDataTable } from "../useSWRDataTable"; +import { SWRDataTableHeader } from "./SWRDataTableHeader"; +import { SWRDataTableBody } from "./SWRDataTableBody"; +import { deserializeQuery } from "#/lib/serializeQuery"; export const formatParamsToDataTable = (params, searchKey) => { const { columnFilters = {}, pageIndex, pageSize, sorting } = params; @@ -75,20 +49,6 @@ export const formatParamsToDataTable = (params, searchKey) => { return to; }; -export const dataTableFetcher = async ([url, paramsObject]) => { - const formattedParams = formatRequestParams(paramsObject); - const params = serializeQuery(formattedParams); - const response = await fetch(`${url}?${params}`, { - headers: { - Accept: "application/json", - }, - }); - if (!response.ok) { - throw new Error("Failed to fetch."); - } - return response.json(); -}; - export const renderDataTableCell = ({ filters, column, row, selectedRows }) => { const value = row.getValue(column.columnDef.accessorKey); @@ -209,7 +169,6 @@ export function SWRDataTable({ selectedRows?: any[]; setSelectedData?: (data: any[]) => void; }) { - const navigate = useNavigate(); const [searchParams] = useSearchParams(); const initialSearch = { ...formatParamsToDataTable( @@ -219,89 +178,13 @@ export function SWRDataTable({ ...defaultParams, }; - const { - pagination, - rowSelection, - columnVisibility, - columnFilters, - sorting, - setPagination, - setRowSelection, - setColumnVisibility, - setColumnFilters, - setSorting, - } = useTableState({ ...initialSearch }); - - const { pageIndex, pageSize } = pagination; - - useEffect(() => { - const formattedParams = formatRequestParams({ - columnFilters, - sorting, - pageIndex, - pageSize, - }); - - const params = serializeQuery(formattedParams); - navigate(`?${params}`, { replace: true }); - }, [pageIndex, pageSize, sorting, columnFilters, navigate]); - - const { data, error } = useSWR( - [fetchPath, { pageIndex, pageSize, sorting, columnFilters }], - dataTableFetcher, - { - keepPreviousData: true, - } - ); - - const columns = buildDataTableColumns( - data?.columns, - data?.filters, - selectedRows + const { data, error, tableState, setTableState } = useSWRDataTable( + fetchPath, + initialSearch ); - const hiddenColumns = columns - .filter((c) => c.hide) - .map((c) => ({ [c.accessorKey]: false })) - .reduce((acc, obj) => Object.assign(acc, obj), {}); - const filters = data?.filters; - const searchFor = data?.search?.key; - - const table = useReactTable({ - data: data?.data ?? [], - pageCount: Math.ceil((data?.total ?? 0) / pageSize), - columns, - state: { - sorting, - columnVisibility: { - ...columnVisibility, - ...hiddenColumns, - }, - rowSelection, - columnFilters, - pagination, - }, - onPaginationChange: setPagination, - manualPagination: true, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }); - - // TODO save selected rows independently of the table page - - useEffect(() => { - if (setSelectedData) - setSelectedData( - table.getSelectedRowModel().flatRows.map((row) => row.original) - ); - }, [rowSelection, table, setSelectedData]); + const buildColumns = (tableColumns, tableFilters) => + buildDataTableColumns(tableColumns, tableFilters, selectedRows); if (error) { return ( @@ -317,68 +200,26 @@ export function SWRDataTable({ } return ( -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - { - if (hasDetails) { - // @ts-expect-error TS(2339) FIXME: Property 'id' does not exist on type 'unknown'. - navigate(`${row.original.id}`); - } - }} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No results found. - - - )} - -
+ +
+ +
+ + + +
+
+
- -
+ ); } diff --git a/src/components/DataTable/TableContext.tsx b/src/components/DataTable/TableContext.tsx new file mode 100644 index 0000000..7f503ad --- /dev/null +++ b/src/components/DataTable/TableContext.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export const TableContext = React.createContext({}); + +export function useTableContext() { + const context = React.useContext(TableContext); + if (context === undefined) { + throw new Error("useTableContext must be used within a TableProvider"); + } + return context; +} diff --git a/src/components/DataTable/index.ts b/src/components/DataTable/index.ts index 212ddb0..abe611e 100644 --- a/src/components/DataTable/index.ts +++ b/src/components/DataTable/index.ts @@ -6,3 +6,7 @@ export * from "./DataTableToolbar"; export * from "./DataTableViewOptions"; export * from "./DynamicActionComponent"; export * from "./SWRDataTable"; +export * from "./TableContext"; +export * from "./useSWRDataTable"; +export * from "./useTableState"; +export * from "./DataTable"; diff --git a/src/components/DataTable/useSWRDataTable.tsx b/src/components/DataTable/useSWRDataTable.tsx new file mode 100644 index 0000000..0f47a40 --- /dev/null +++ b/src/components/DataTable/useSWRDataTable.tsx @@ -0,0 +1,54 @@ +import useSWR from "swr"; +import { useTableState } from "./useTableState"; +import { serializeQuery } from "#/lib/serializeQuery"; + +function formatRequestParams(originalObj) { + return { + ...originalObj, + sorting: originalObj?.sorting?.length > 0 ? originalObj.sorting[0] : null, + columnFilters: originalObj.columnFilters.reduce((acc, filter) => { + acc[filter.id] = filter.value; + return acc; + }, {}), + }; +} + +const dataTableFetcher = async ([url, paramsObject]) => { + const formattedParams = formatRequestParams(paramsObject); + const params = serializeQuery(formattedParams); + const response = await fetch(`${url}?${params}`, { + headers: { + Accept: "application/json", + }, + }); + if (!response.ok) { + throw new Error("Failed to fetch."); + } + return response.json(); +}; + +export function useSWRDataTable(path, initialSearch = {}, options = {}) { + const { tableState, setTableState } = useTableState({ ...initialSearch }); + + const { data, error, isLoading } = useSWR( + [ + path, + { + pageIndex: tableState.pagination.pageIndex, + pageSize: tableState.pagination.pageSize, + sorting: tableState.sorting, + columnFilters: tableState.columnFilters, + }, + ], + dataTableFetcher, + { keepPreviousData: true, ...options } + ); + + return { + data, + error, + isLoading, + tableState, + setTableState, + }; +} diff --git a/src/components/DataTable/SWRDataTable/useTableState.tsx b/src/components/DataTable/useTableState.tsx similarity index 94% rename from src/components/DataTable/SWRDataTable/useTableState.tsx rename to src/components/DataTable/useTableState.tsx index 55f7c26..ad7120f 100644 --- a/src/components/DataTable/SWRDataTable/useTableState.tsx +++ b/src/components/DataTable/useTableState.tsx @@ -46,5 +46,8 @@ export function useTableState( ] ); - return { ...state, ...handlers }; + return { + tableState: state, + setTableState: handlers, + }; }