Skip to content

Commit

Permalink
refactor(react-table): allow conditional filters on useTable (#5862)
Browse files Browse the repository at this point in the history
  • Loading branch information
aliemir committed Apr 24, 2024
1 parent c2ef59b commit 7c22b8e
Show file tree
Hide file tree
Showing 10 changed files with 630 additions and 48 deletions.
28 changes: 28 additions & 0 deletions .changeset/selfish-taxis-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@refinedev/react-table": patch
---

fix: updated column filter transformation logic to handle conditional filters

`useTable` hook was ignoring the conditional filters with `"and"` and `"or"` operators, causing custom filtering logic inside the table to not work as expected and omitting the filters from the query. This PR enables working with conditionenal filters and fixes the disappearing filters issue.

To customize the `key` value of the conditional filter, you can use the `filterKey` property in the column's `meta` property. This property will be used as the key for the filter when setting the filter value to the table from `@refinedev/core`'s filter state and when setting the filter value to the filter state from the table.

```tsx
// An example of how to use the `filterKey` property in the column's `meta` property
const columns: ColumnDef<IPost> = [
{
id: "title",
header: "Title",
accessorKey: "title",
meta: {
// This is optional, if not defined column id will be used as the key
filterKey: "titleFilter",
// If operator is not `'eq'` or `'in'`, make sure to set the `filterOperator` property
filterOperator: "and",
},
},
];
```

Resolves [#5856](https://github.com/refinedev/refine/issues/5856)
4 changes: 4 additions & 0 deletions documentation/docs/packages/tanstack-table/use-table/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ It also syncs the filtering state with the URL if you enable the [`syncWithLocat

By default, filter operators are set to "eq" for all fields. You can specify which field will be filtered with which filter operator with the `filterOperator` property in the `meta` object. Just be aware that the `filterOperator` must be a [`CrudOperators`](/docs/core/interface-references#crudoperators) type.

If you're going to use [logical filters](/docs/core/interface-references#logicalfilter) (`and`, `or`), you can set `filterOperator` to `and` or `or` in the `meta` object. By design, logical filters do not have `field` property, instead they have `key` property to differentiate between filters if there are multiple filters with the same operator.

By default, `id` field of the column is used as the `key` property. If you want to use a different field as the `key`, you can set the `filterKey` property in the `meta` object.

<FilteringLivePreview/>

## Realtime Updates
Expand Down
69 changes: 21 additions & 48 deletions packages/react-table/src/useTable/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect } from "react";
import {
BaseRecord,
ConditionalFilter,
CrudFilter,
CrudOperators,
HttpError,
LogicalFilter,
Expand All @@ -18,7 +20,12 @@ import {
getFilteredRowModel,
} from "@tanstack/react-table";

import { useIsFirstRender } from "../utils";
import {
useIsFirstRender,
columnFiltersToCrudFilters,
getRemovedFilters,
crudFiltersToColumnFilters,
} from "../utils";

export type UseTableReturnType<
TData extends BaseRecord = BaseRecord,
Expand Down Expand Up @@ -80,17 +87,6 @@ export function useTable<
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(),
Expand All @@ -109,11 +105,10 @@ export function useTable<
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,
})),
columnFilters: crudFiltersToColumnFilters({
columns: rest.columns,
crudFilters: filtersCore,
}),
...reactTableInitialState,
},
pageCount,
Expand Down Expand Up @@ -156,46 +151,24 @@ export function useTable<
}, [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 crudFilters: CrudFilter[] = columnFiltersToCrudFilters({
columns,
columnFilters,
});

const filteredArray = logicalFilters.filter(
(value) =>
!crudFilters.some(
(b) => value.field === b.field && value.operator === b.operator,
),
crudFilters.push(
...getRemovedFilters({
nextFilters: crudFilters,
coreFilters: filtersCore,
}),
);

filteredArray?.map((filter) => {
crudFilters.push({
field: filter.field,
operator: filter.operator,
value: undefined,
});
});

setFilters(crudFilters);

if (crudFilters.length > 0 && isPaginationEnabled && !isFirstRender) {
setCurrent(1);
}
}, [columnFilters]);
}, [columnFilters, columns]);

return {
...reactTableResult,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { ColumnDef, ColumnFilter } from "@tanstack/react-table";
import { columnFiltersToCrudFilters } from ".";

describe("columnFiltersToCrudFilters", () => {
it("should return with crud filters type", () => {
const columns: ColumnDef<any, any>[] = [
{
id: "name",
header: "Name",
accessorKey: "name",
},
{
header: "Age",
accessorKey: "age",
},
];

const columnFilters: ColumnFilter[] = [
{ id: "name", value: "John" },
{ id: "age", value: 30 },
];

const crudFilters = columnFiltersToCrudFilters({
columns,
columnFilters,
});

expect(crudFilters).toEqual([
{ field: "name", value: "John", operator: "eq" },
{ field: "age", value: 30, operator: "eq" },
]);
});

it("should set operator to 'in' if value is an array", () => {
const columns: ColumnDef<any, any>[] = [
{
id: "name",
header: "Name",
accessorKey: "name",
},
{
header: "Age",
accessorKey: "age",
},
];

const columnFilters: ColumnFilter[] = [
{ id: "name", value: ["John", "Doe"] },
{ id: "age", value: 30 },
];

const crudFilters = columnFiltersToCrudFilters({
columns,
columnFilters,
});

expect(crudFilters).toEqual([
{ field: "name", value: ["John", "Doe"], operator: "in" },
{ field: "age", value: 30, operator: "eq" },
]);
});

it("should respect filterOperator from column meta", () => {
const columns: ColumnDef<any, any>[] = [
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "contains",
},
},
{
header: "Age",
accessorKey: "age",
},
];

const columnFilters: ColumnFilter[] = [
{ id: "name", value: "John" },
{ id: "age", value: 30 },
];

const crudFilters = columnFiltersToCrudFilters({
columns,
columnFilters,
});

expect(crudFilters).toEqual([
{ field: "name", value: "John", operator: "contains" },
{ field: "age", value: 30, operator: "eq" },
]);
});

it("should transform and/or filters to conditional filters", () => {
const columns: ColumnDef<any, any>[] = [
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "or",
},
},
{
id: "age",
header: "Age",
accessorKey: "age",
meta: {
filterOperator: "and",
},
},
];

const columnFilters: ColumnFilter[] = [
{
id: "name",
value: [
{ field: "name", operator: "eq", value: "John" },
{ field: "name", operator: "eq", value: "Doe" },
],
},

{
id: "age",
value: [
{ field: "age", operator: "gte", value: 18 },
{ field: "age", operator: "lte", value: 65 },
],
},
];

const crudFilters = columnFiltersToCrudFilters({
columns,
columnFilters,
});

expect(crudFilters).toEqual([
{
key: "name",
operator: "or",
value: [
{ field: "name", operator: "eq", value: "John" },
{ field: "name", operator: "eq", value: "Doe" },
],
},
{
key: "age",
operator: "and",
value: [
{ field: "age", operator: "gte", value: 18 },
{ field: "age", operator: "lte", value: 65 },
],
},
]);
});

it("should respect custom filter key for conditional filters", () => {
const columns: ColumnDef<any, any>[] = [
{
id: "name",
header: "Name",
accessorKey: "name",
meta: {
filterOperator: "or",
},
},
{
id: "age",
header: "Age",
accessorKey: "age",
meta: {
filterOperator: "and",
filterKey: "customKey",
},
},
];

const columnFilters: ColumnFilter[] = [
{
id: "name",
value: [
{ field: "name", operator: "eq", value: "John" },
{ field: "name", operator: "eq", value: "Doe" },
],
},

{
id: "age",
value: [
{ field: "age", operator: "gte", value: 18 },
{ field: "age", operator: "lte", value: 65 },
],
},
];

const crudFilters = columnFiltersToCrudFilters({
columns,
columnFilters,
});

expect(crudFilters).toEqual([
{
key: "name",
operator: "or",
value: [
{ field: "name", operator: "eq", value: "John" },
{ field: "name", operator: "eq", value: "Doe" },
],
},
{
key: "customKey",
operator: "and",
value: [
{ field: "age", operator: "gte", value: 18 },
{ field: "age", operator: "lte", value: 65 },
],
},
]);
});
});

0 comments on commit 7c22b8e

Please sign in to comment.