Presupuestador_web/client/src/lib/hooks/useDataTable/useDataTable.tsx

322 lines
8.7 KiB
TypeScript
Raw Normal View History

2024-06-06 11:05:54 +00:00
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 };
}