fork refine

This commit is contained in:
Stefan Pejcic
2024-02-05 10:23:04 +01:00
parent 3fffde9a8f
commit 8496a83edb
3634 changed files with 715528 additions and 2 deletions

View File

@@ -0,0 +1,474 @@
import { renderHook, waitFor } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import { ColumnDef } from "@tanstack/react-table";
import { TestWrapper } from "../../test";
import { useTable } from "./index";
type Post = {
id: number;
title: string;
};
const columns: ColumnDef<Post>[] = [
{
id: "id",
header: "ID",
accessorKey: "id",
},
{
id: "title",
header: "Title",
accessorKey: "title",
meta: {
filterOperator: "contains",
},
},
];
describe("useTable Hook", () => {
it("It should work successfully with no properties", async () => {
const { result } = renderHook(
() => useTable({ columns, refineCoreProps: { resource: "posts" } }),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(
() => {
expect(
!result.current.refineCore.tableQueryResult.isLoading,
).toBeTruthy();
},
{ timeout: 10000 },
);
const {
options: { state, pageCount },
refineCore: { tableQueryResult },
} = result.current;
expect(pageCount).toBe(1);
expect(state.pagination?.pageIndex).toBe(0);
expect(state.pagination?.pageSize).toBe(10);
expect(state.columnFilters).toEqual([]);
expect(state.sorting).toEqual([]);
expect(tableQueryResult.data?.data).toHaveLength(3);
expect(tableQueryResult.data?.total).toBe(3);
});
it("It should work successfully with initialCurrent and initialPageSize", async () => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
resource: "posts",
pagination: {
current: 2,
pageSize: 1,
},
},
}),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isLoading,
).toBeTruthy();
});
const {
options: { state, pageCount },
refineCore: { pageSize: corePageSize, current },
} = result.current;
expect(corePageSize).toBe(1);
expect(current).toBe(2);
expect(state.pagination?.pageIndex).toBe(1);
expect(state.pagination?.pageSize).toBe(1);
expect(pageCount).toBe(3);
});
it("It should work successfully with initialFilter", async () => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
resource: "posts",
filters: {
initial: [
{
field: "title",
operator: "contains",
value: "Hello2",
},
{
field: "active",
operator: "eq",
value: true,
},
],
},
},
}),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isLoading,
).toBeTruthy();
});
const {
options: { state },
getColumn,
refineCore: { filters: filtersCore },
} = result.current;
expect(filtersCore).toEqual([
{ field: "title", value: "Hello2", operator: "contains" },
{ field: "active", value: true, operator: "eq" },
]);
expect(state.columnFilters).toEqual([
{ id: "title", value: "Hello2", operator: "contains" },
{ id: "active", value: true, operator: "eq" },
]);
act(() => {
const titleColumn = getColumn("title");
titleColumn?.setFilterValue("Hello");
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.filters).toEqual([
{ field: "title", value: "Hello", operator: "contains" },
{ field: "active", value: true, operator: "eq" },
]);
expect(result.current.options.state.columnFilters).toEqual([
{ id: "title", value: "Hello" },
{ id: "active", operator: "eq", value: true },
]);
act(() => {
const titleColumn = getColumn("title");
titleColumn?.setFilterValue(undefined);
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.filters).toEqual([
{ field: "active", value: true, operator: "eq" },
]);
expect(result.current.options.state.columnFilters).toEqual([
{ id: "active", operator: "eq", value: true },
]);
});
it("It should work successfully with initialSorter", async () => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
sorters: {
initial: [
{ field: "id", order: "asc" },
{ field: "title", order: "desc" },
],
},
},
}),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
const {
options: { state },
setSorting,
refineCore: { sorter },
} = result.current;
expect(sorter).toEqual([
{ field: "id", order: "asc" },
{ field: "title", order: "desc" },
]);
expect(state.sorting).toEqual([
{ id: "id", desc: false },
{ id: "title", desc: true },
]);
act(() => {
setSorting([
{ id: "title", desc: false },
{ id: "id", desc: true },
]);
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.sorter).toEqual([
{ field: "title", order: "asc" },
{ field: "id", order: "desc" },
]);
expect(result.current.options.state.sorting).toEqual([
{ id: "title", desc: false },
{ id: "id", desc: true },
]);
});
it("It should work successfully with initialFilter and permanentFilter", async () => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
filters: {
initial: [
{
field: "title",
operator: "contains",
value: "Hello",
},
],
permanent: [
{
field: "category.id",
operator: "eq",
value: 1,
},
],
},
},
}),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
const {
refineCore: { filters: filtersCore },
options: { state },
getColumn,
} = result.current;
expect(filtersCore).toEqual([
{ field: "category.id", operator: "eq", value: 1 },
{ field: "title", value: "Hello", operator: "contains" },
]);
expect(state.columnFilters).toEqual([
{ id: "title", operator: "contains", value: "Hello" },
{ id: "category.id", operator: "eq", value: 1 },
]);
act(() => {
const titleColumn = getColumn("title");
titleColumn?.setFilterValue("Test");
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.filters).toEqual([
{ field: "category.id", value: 1, operator: "eq" },
{ field: "title", value: "Test", operator: "contains" },
]);
expect(result.current.options.state.columnFilters).toEqual([
{ id: "title", value: "Test" },
{ id: "category.id", operator: "eq", value: 1 },
]);
act(() => {
const titleColumn = getColumn("title");
titleColumn?.setFilterValue(undefined);
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.filters).toEqual([
{ field: "category.id", value: 1, operator: "eq" },
]);
expect(result.current.options.state.columnFilters).toEqual([
{ id: "category.id", operator: "eq", value: 1 },
]);
});
it("It should work successfully with initialSorter and permanentSorter", async () => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
sorters: {
initial: [
{
field: "title",
order: "asc",
},
],
permanent: [
{
field: "category.id",
order: "desc",
},
],
},
},
}),
{
wrapper: TestWrapper({
routerInitialEntries: ["/posts"],
}),
},
);
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
const {
getColumn,
refineCore: { sorter },
options: { state },
} = result.current;
expect(sorter).toEqual([
{ field: "category.id", order: "desc" },
{ field: "title", order: "asc" },
]);
expect(state.sorting).toEqual([
{ id: "title", desc: false },
{ id: "category.id", desc: true },
]);
act(() => {
const titleColumn = getColumn("title");
titleColumn?.toggleSorting(true, true);
});
await waitFor(() => {
expect(
!result.current.refineCore.tableQueryResult.isFetching,
).toBeTruthy();
});
expect(result.current.refineCore.sorter).toEqual([
{ field: "category.id", order: "desc" },
{ field: "title", order: "desc" },
]);
expect(result.current.options.state.sorting).toEqual([
{ id: "title", desc: true },
{ id: "category.id", desc: true },
]);
});
it.each(["off", "server"] as const)(
"when sorters.mode is %s, should set sortingMode to %s",
async (mode) => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
sorters: {
mode,
},
},
}),
{
wrapper: TestWrapper({}),
},
);
expect(result.current.options.getSortedRowModel).toEqual(
mode === "server" ? undefined : expect.any(Function),
);
expect(result.current.options.manualSorting).toEqual(
mode === "server" ? true : false,
);
},
);
it.each(["off", "server"] as const)(
"when filters.mode is %s, should set sortingMode to %s",
async (mode) => {
const { result } = renderHook(
() =>
useTable({
columns,
refineCoreProps: {
filters: {
mode,
},
},
}),
{
wrapper: TestWrapper({}),
},
);
expect(result.current.options.getFilteredRowModel).toEqual(
mode === "server" ? undefined : expect.any(Function),
);
expect(result.current.options.manualFiltering).toEqual(
mode === "server" ? true : false,
);
},
);
});

View File

@@ -0,0 +1,210 @@
import { useEffect } from "react";
import {
BaseRecord,
CrudOperators,
HttpError,
LogicalFilter,
useTable as useTableCore,
useTableProps as useTablePropsCore,
useTableReturnType as useTableReturnTypeCore,
} from "@refinedev/core";
import {
useReactTable,
TableOptions,
Table,
getCoreRowModel,
ColumnFilter,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/react-table";
import { useIsFirstRender } from "../utils";
export type UseTableReturnType<
TData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
> = Table<TData> & {
refineCore: useTableReturnTypeCore<TData, TError>;
};
export type UseTableProps<
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TData extends BaseRecord = TQueryFnData,
> = {
/**
* Configuration object for the core of the [useTable](/docs/api-reference/core/hooks/useTable/)
* @type [`useTablePropsCore<TQueryFnData, TError>`](/docs/api-reference/core/hooks/useTable/#properties)
*/
refineCoreProps?: useTablePropsCore<TQueryFnData, TError, TData>;
} & Pick<TableOptions<TData>, "columns"> &
Partial<Omit<TableOptions<TData>, "columns">>;
export function useTable<
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TData extends BaseRecord = TQueryFnData,
>({
refineCoreProps: { hasPagination = true, ...refineCoreProps } = {},
initialState: reactTableInitialState = {},
...rest
}: UseTableProps<TQueryFnData, TError, TData>): UseTableReturnType<
TData,
TError
> {
const isFirstRender = useIsFirstRender();
const useTableResult = useTableCore<TQueryFnData, TError, TData>({
...refineCoreProps,
hasPagination,
});
const isServerSideFilteringEnabled =
(refineCoreProps.filters?.mode || "server") === "server";
const isServerSideSortingEnabled =
(refineCoreProps.sorters?.mode || "server") === "server";
const hasPaginationString = hasPagination === false ? "off" : "server";
const isPaginationEnabled =
(refineCoreProps.pagination?.mode ?? hasPaginationString) !== "off";
const {
tableQueryResult: { data },
current,
setCurrent,
pageSize: pageSizeCore,
setPageSize: setPageSizeCore,
sorters,
setSorters,
filters: filtersCore,
setFilters,
pageCount,
} = useTableResult;
const logicalFilters: LogicalFilter[] = [];
filtersCore.forEach((filter) => {
if (
filter.operator !== "or" &&
filter.operator !== "and" &&
"field" in filter
) {
logicalFilters.push(filter);
}
});
const reactTableResult = useReactTable<TData>({
data: data?.data ?? [],
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: isServerSideSortingEnabled
? undefined
: getSortedRowModel(),
getFilteredRowModel: isServerSideFilteringEnabled
? undefined
: getFilteredRowModel(),
initialState: {
pagination: {
pageIndex: current - 1,
pageSize: pageSizeCore,
},
sorting: sorters.map((sorting) => ({
id: sorting.field,
desc: sorting.order === "desc",
})),
columnFilters: logicalFilters.map((filter) => ({
id: filter.field,
operator: filter.operator as Exclude<
CrudOperators,
"or" | "and"
>,
value: filter.value,
})),
...reactTableInitialState,
},
pageCount,
manualPagination: true,
manualSorting: isServerSideSortingEnabled,
manualFiltering: isServerSideFilteringEnabled,
...rest,
});
const { state, columns } = reactTableResult.options;
const { pagination, sorting, columnFilters } = state;
const { pageIndex, pageSize } = pagination ?? {};
useEffect(() => {
if (pageIndex !== undefined) {
setCurrent(pageIndex + 1);
}
}, [pageIndex]);
useEffect(() => {
if (pageSize !== undefined) {
setPageSizeCore(pageSize);
}
}, [pageSize]);
useEffect(() => {
if (sorting !== undefined) {
setSorters(
sorting?.map((sorting) => ({
field: sorting.id,
order: sorting.desc ? "desc" : "asc",
})),
);
if (sorting.length > 0 && isPaginationEnabled && !isFirstRender) {
setCurrent(1);
}
}
}, [sorting]);
useEffect(() => {
const crudFilters: LogicalFilter[] = [];
columnFilters?.map((filter) => {
const operator =
(
filter as ColumnFilter & {
operator?: Exclude<CrudOperators, "or" | "and">;
}
).operator ??
((columns.find((c) => c.id === filter.id) as any)?.meta
?.filterOperator as Exclude<CrudOperators, "or" | "and">);
crudFilters.push({
field: filter.id,
operator:
operator ?? (Array.isArray(filter.value) ? "in" : "eq"),
value: filter.value,
});
});
const filteredArray = logicalFilters.filter(
(value) =>
!crudFilters.some(
(b) =>
value.field === b.field &&
value.operator === b.operator,
),
);
filteredArray?.map((filter) => {
crudFilters.push({
field: filter.field,
operator: filter.operator,
value: undefined,
});
});
setFilters(crudFilters);
if (crudFilters.length > 0 && isPaginationEnabled && !isFirstRender) {
setCurrent(1);
}
}, [columnFilters]);
return {
...reactTableResult,
refineCore: useTableResult,
};
}