322 lines
8.7 KiB
TypeScript
322 lines
8.7 KiB
TypeScript
|
|
import {
|
||
|
|
OnChangeFn,
|
||
|
|
PaginationState,
|
||
|
|
getCoreRowModel,
|
||
|
|
getFacetedRowModel,
|
||
|
|
getFacetedUniqueValues,
|
||
|
|
getFilteredRowModel,
|
||
|
|
getSortedRowModel,
|
||
|
|
useReactTable,
|
||
|
|
type ColumnDef,
|
||
|
|
type ColumnFiltersState,
|
||
|
|
type SortingState,
|
||
|
|
type VisibilityState,
|
||
|
|
} from "@tanstack/react-table";
|
||
|
|
|
||
|
|
import { getDataTableSelectionColumn } from "@/components";
|
||
|
|
import React, { useCallback, useMemo, useState } from "react";
|
||
|
|
import { useSearchParams } from "react-router-dom";
|
||
|
|
import { DataTableFilterField } from "../../types";
|
||
|
|
import { usePaginationParams } from "../usePagination";
|
||
|
|
|
||
|
|
//import { useDebounce } from "@/hooks/use-debounce";
|
||
|
|
|
||
|
|
interface UseDataTableProps<TData, TValue> {
|
||
|
|
/**
|
||
|
|
* The data for the table.
|
||
|
|
* @default []
|
||
|
|
* @type TData[]
|
||
|
|
*/
|
||
|
|
data: TData[];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The columns of the table.
|
||
|
|
* @default []
|
||
|
|
* @type ColumnDef<TData, TValue>[]
|
||
|
|
*/
|
||
|
|
columns: ColumnDef<TData, TValue>[];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The number of pages in the table.
|
||
|
|
* @type number
|
||
|
|
*/
|
||
|
|
pageCount: number;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enable sorting columns.
|
||
|
|
* @default false
|
||
|
|
* @type boolean
|
||
|
|
*/
|
||
|
|
enableSorting?: boolean;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enable hiding columns.
|
||
|
|
* @default false
|
||
|
|
* @type boolean
|
||
|
|
*/
|
||
|
|
enableHiding?: boolean;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enable selection rows.
|
||
|
|
* @default false
|
||
|
|
* @type boolean
|
||
|
|
*/
|
||
|
|
enableRowSelection?: boolean;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Defines filter fields for the table. Supports both dynamic faceted filters and search filters.
|
||
|
|
* - Faceted filters are rendered when `options` are provided for a filter field.
|
||
|
|
* - Otherwise, search filters are rendered.
|
||
|
|
*
|
||
|
|
* The indie filter field `value` represents the corresponding column name in the database table.
|
||
|
|
* @default []
|
||
|
|
* @type { label: string, value: keyof TData, placeholder?: string, options?: { label: string, value: string, icon?: React.ComponentType<{ className?: string }> }[] }[]
|
||
|
|
* @example
|
||
|
|
* ```ts
|
||
|
|
* // Render a search filter
|
||
|
|
* const filterFields = [
|
||
|
|
* { label: "Title", value: "title", placeholder: "Search titles" }
|
||
|
|
* ];
|
||
|
|
* // Render a faceted filter
|
||
|
|
* const filterFields = [
|
||
|
|
* {
|
||
|
|
* label: "Status",
|
||
|
|
* value: "status",
|
||
|
|
* options: [
|
||
|
|
* { label: "Todo", value: "todo" },
|
||
|
|
* { label: "In Progress", value: "in-progress" },
|
||
|
|
* { label: "Done", value: "done" },
|
||
|
|
* { label: "Canceled", value: "canceled" }
|
||
|
|
* ]
|
||
|
|
* }
|
||
|
|
* ];
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
filterFields?: DataTableFilterField<TData>[];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enable notion like column filters.
|
||
|
|
* Advanced filters and column filters cannot be used at the same time.
|
||
|
|
* @default false
|
||
|
|
* @type boolean
|
||
|
|
*/
|
||
|
|
enableAdvancedFilter?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useDataTable<TData, TValue>({
|
||
|
|
data,
|
||
|
|
columns,
|
||
|
|
pageCount,
|
||
|
|
enableSorting = false,
|
||
|
|
enableHiding = false,
|
||
|
|
enableRowSelection = false,
|
||
|
|
filterFields = [],
|
||
|
|
enableAdvancedFilter = false,
|
||
|
|
}: UseDataTableProps<TData, TValue>) {
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
||
|
|
|
||
|
|
const [pagination, setPagination] = usePaginationParams();
|
||
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||
|
|
|
||
|
|
// Memoize computation of searchableColumns and filterableColumns
|
||
|
|
const { searchableColumns, filterableColumns } = useMemo(() => {
|
||
|
|
return {
|
||
|
|
searchableColumns: filterFields.filter((field) => !field.options),
|
||
|
|
filterableColumns: filterFields.filter((field) => field.options),
|
||
|
|
};
|
||
|
|
}, [filterFields]);
|
||
|
|
|
||
|
|
// Create query string
|
||
|
|
/*const createQueryString = useCallback(
|
||
|
|
(params: Record<string, string | number | null>) => {
|
||
|
|
const newSearchParams = new URLSearchParams(searchParams?.toString());
|
||
|
|
|
||
|
|
for (const [key, value] of Object.entries(params)) {
|
||
|
|
if (value === null) {
|
||
|
|
newSearchParams.delete(key);
|
||
|
|
} else {
|
||
|
|
newSearchParams.set(key, String(value));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return newSearchParams.toString();
|
||
|
|
},
|
||
|
|
[searchParams]
|
||
|
|
);*/
|
||
|
|
|
||
|
|
// Initial column filters
|
||
|
|
const initialColumnFilters: ColumnFiltersState = useMemo(() => {
|
||
|
|
return Array.from(searchParams.entries()).reduce<ColumnFiltersState>(
|
||
|
|
(filters, [key, value]) => {
|
||
|
|
const filterableColumn = filterableColumns.find(
|
||
|
|
(column) => column.value === key
|
||
|
|
);
|
||
|
|
const searchableColumn = searchableColumns.find(
|
||
|
|
(column) => column.value === key
|
||
|
|
);
|
||
|
|
|
||
|
|
if (filterableColumn) {
|
||
|
|
filters.push({
|
||
|
|
id: key,
|
||
|
|
value: value.split("."),
|
||
|
|
});
|
||
|
|
} else if (searchableColumn) {
|
||
|
|
filters.push({
|
||
|
|
id: key,
|
||
|
|
value: [value],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return filters;
|
||
|
|
},
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
}, [filterableColumns, searchableColumns, searchParams]);
|
||
|
|
|
||
|
|
// Table states
|
||
|
|
const [rowSelection, setRowSelection] = React.useState({});
|
||
|
|
|
||
|
|
const [columnVisibility, setColumnVisibility] =
|
||
|
|
React.useState<VisibilityState>({});
|
||
|
|
|
||
|
|
const [columnFilters, setColumnFilters] =
|
||
|
|
React.useState<ColumnFiltersState>(initialColumnFilters);
|
||
|
|
|
||
|
|
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
|
||
|
|
if (typeof updater === "function") {
|
||
|
|
const newPagination = updater(pagination);
|
||
|
|
console.log(newPagination);
|
||
|
|
setPagination(newPagination);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const sortingUpdater: OnChangeFn<SortingState> = (updater) => {
|
||
|
|
if (typeof updater === "function") {
|
||
|
|
setSorting(updater(sorting));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle server-side filtering
|
||
|
|
/*const debouncedSearchableColumnFilters = JSON.parse(
|
||
|
|
useDebounce(
|
||
|
|
JSON.stringify(
|
||
|
|
columnFilters.filter((filter) => {
|
||
|
|
return searchableColumns.find((column) => column.value === filter.id);
|
||
|
|
})
|
||
|
|
),
|
||
|
|
500
|
||
|
|
)
|
||
|
|
) as ColumnFiltersState;*/
|
||
|
|
|
||
|
|
/*const filterableColumnFilters = columnFilters.filter((filter) => {
|
||
|
|
return filterableColumns.find((column) => column.value === filter.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
const [mounted, setMounted] = useState(false);*/
|
||
|
|
|
||
|
|
/*useEffect(() => {
|
||
|
|
// Opt out when advanced filter is enabled, because it contains additional params
|
||
|
|
if (enableAdvancedFilter) return;
|
||
|
|
|
||
|
|
// Prevent resetting the page on initial render
|
||
|
|
if (!mounted) {
|
||
|
|
setMounted(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize new params
|
||
|
|
const newParamsObject = {
|
||
|
|
page: 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle debounced searchable column filters
|
||
|
|
for (const column of debouncedSearchableColumnFilters) {
|
||
|
|
if (typeof column.value === "string") {
|
||
|
|
Object.assign(newParamsObject, {
|
||
|
|
[column.id]: typeof column.value === "string" ? column.value : null,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle filterable column filters
|
||
|
|
for (const column of filterableColumnFilters) {
|
||
|
|
if (typeof column.value === "object" && Array.isArray(column.value)) {
|
||
|
|
Object.assign(newParamsObject, { [column.id]: column.value.join(".") });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove deleted values
|
||
|
|
for (const key of searchParams.keys()) {
|
||
|
|
if (
|
||
|
|
(searchableColumns.find((column) => column.value === key) &&
|
||
|
|
!debouncedSearchableColumnFilters.find(
|
||
|
|
(column) => column.id === key
|
||
|
|
)) ||
|
||
|
|
(filterableColumns.find((column) => column.value === key) &&
|
||
|
|
!filterableColumnFilters.find((column) => column.id === key))
|
||
|
|
) {
|
||
|
|
Object.assign(newParamsObject, { [key]: null });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// After cumulating all the changes, push new params
|
||
|
|
navigate(`${location.pathname}?${createQueryString(newParamsObject)}`);
|
||
|
|
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, [
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
//JSON.stringify(debouncedSearchableColumnFilters),
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
JSON.stringify(filterableColumnFilters),
|
||
|
|
]);*/
|
||
|
|
|
||
|
|
const getTableColumns = useCallback(() => {
|
||
|
|
const _columns = columns;
|
||
|
|
if (enableRowSelection) {
|
||
|
|
_columns.unshift(getDataTableSelectionColumn());
|
||
|
|
}
|
||
|
|
return _columns;
|
||
|
|
}, [columns, enableRowSelection]);
|
||
|
|
|
||
|
|
const table = useReactTable({
|
||
|
|
columns: getTableColumns(),
|
||
|
|
data,
|
||
|
|
getCoreRowModel: getCoreRowModel(),
|
||
|
|
//getPaginationRowModel: getPaginationRowModel(),
|
||
|
|
|
||
|
|
state: {
|
||
|
|
pagination,
|
||
|
|
sorting,
|
||
|
|
columnVisibility,
|
||
|
|
rowSelection,
|
||
|
|
columnFilters,
|
||
|
|
},
|
||
|
|
|
||
|
|
enableRowSelection,
|
||
|
|
onRowSelectionChange: setRowSelection,
|
||
|
|
|
||
|
|
manualSorting: true,
|
||
|
|
enableSorting,
|
||
|
|
getSortedRowModel: getSortedRowModel(),
|
||
|
|
onSortingChange: sortingUpdater,
|
||
|
|
|
||
|
|
enableHiding,
|
||
|
|
onColumnVisibilityChange: setColumnVisibility,
|
||
|
|
|
||
|
|
manualPagination: true,
|
||
|
|
pageCount: pageCount ?? -1,
|
||
|
|
onPaginationChange: paginationUpdater,
|
||
|
|
|
||
|
|
getFilteredRowModel: getFilteredRowModel(),
|
||
|
|
|
||
|
|
manualFiltering: true,
|
||
|
|
onColumnFiltersChange: setColumnFilters,
|
||
|
|
|
||
|
|
getFacetedRowModel: getFacetedRowModel(),
|
||
|
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||
|
|
});
|
||
|
|
|
||
|
|
return { table };
|
||
|
|
}
|