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,
+ };
}