This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`definitions/table sorters should be prioritized over sorter 1`] = `"current=1&pageSize=10&sorters[0][field]=id&sorters[0][order]=desc"`;
exports[`definitions/table stringify table params correctly 1`] = `"foo=bar&current=1&pageSize=10&sorters[0][field]=id&sorters[0][order]=desc&sorters[1][field]=title&sorters[1][order]=desc&filters[0][field]=categoryId&filters[0][operator]=in&filters[0][value][0]=1&filters[0][value][1]=2"`;
exports[`definitions/table stringify table single sort params correctly 1`] = `"current=1&pageSize=10&sorters[0][field]=id&sorters[0][order]=desc&filters[0][field]=categoryId&filters[0][operator]=in&filters[0][value][0]=1&filters[0][value][1]=2"`;

View File

@@ -0,0 +1,639 @@
import type { CrudFilter, CrudSort } from "../../contexts/data/types";
import {
compareFilters,
compareSorters,
getDefaultFilter,
getDefaultSortOrder,
parseTableParams,
parseTableParamsFromQuery,
stringifyTableParams,
unionFilters,
unionSorters,
} from "./";
describe("definitions/table", () => {
it("getDefaultSortOrder", () => {
const sorter: CrudSort[] = [
{
field: "title",
order: "asc",
},
{
field: "view",
order: "desc",
},
];
expect(getDefaultSortOrder("title", sorter)).toEqual("asc");
expect(getDefaultSortOrder("view", sorter)).toEqual("desc");
});
it("getDefaultSortOrder pass empty sorter", () => {
expect(getDefaultSortOrder("title", undefined)).toEqual(undefined);
});
it("getDefaultSortOrder pass different column name", () => {
expect(
getDefaultSortOrder("title", [
{
field: "foo",
order: "asc",
},
]),
).toEqual(undefined);
});
it("getDefaultFilter", () => {
const filters: CrudFilter[] = [
{
field: "title",
operator: "contains",
value: "test",
},
];
expect(getDefaultFilter("title", filters, "contains")).toEqual("test");
});
it("getDefaultFilter empty array", () => {
const filters: CrudFilter[] = [
{
field: "title",
operator: "contains",
value: undefined,
},
];
expect(getDefaultFilter("title", filters, "contains")).toEqual([]);
});
it("getDefaultFilter default operator", () => {
const filters: CrudFilter[] = [
{
field: "title",
operator: "eq",
value: "test",
},
];
expect(getDefaultFilter("title", filters)).toEqual("test");
});
it("stringify table params correctly", async () => {
const pagination = {
current: 1,
pageSize: 10,
};
const sorters: CrudSort[] = [
{
field: "id",
order: "desc",
},
{
field: "title",
order: "desc",
},
];
const filters: CrudFilter[] = [
{
field: "categoryId",
operator: "in",
value: [1, 2],
},
];
const userDefinedQueryParam = {
foo: "bar",
};
const url = stringifyTableParams({
pagination,
sorters,
filters,
...userDefinedQueryParam,
});
expect(url).toMatchSnapshot();
});
it("stringify table single sort params correctly", async () => {
const pagination = {
current: 1,
pageSize: 10,
};
const sorters: CrudSort[] = [{ field: "id", order: "desc" }];
const filters: CrudFilter[] = [
{
field: "categoryId",
operator: "in",
value: [1, 2],
},
];
const url = stringifyTableParams({ pagination, sorters, filters });
expect(url).toMatchSnapshot();
});
it("parse table params with single sorter correctly", async () => {
const url =
"?current=1&pageSize=10&sorter[0][field]=id&sorter[0][order]=desc&filters[0][operator]=in&filters[0][field]=categoryId&filters[0][value][0]=1&filters[0][value][1]=2";
const { parsedCurrent, parsedPageSize, parsedSorter, parsedFilters } =
parseTableParams(url);
expect(parsedCurrent).toBe(1);
expect(parsedPageSize).toBe(10);
expect(parsedSorter).toStrictEqual([{ field: "id", order: "desc" }]);
expect(parsedFilters).toStrictEqual([
{ field: "categoryId", operator: "in", value: ["1", "2"] },
]);
});
it("sorters should be prioritized over sorter", async () => {
const pagination = {
current: 1,
pageSize: 10,
};
const sorters: CrudSort[] = [{ field: "id", order: "desc" }];
const sorter: CrudSort[] = [{ field: "id2", order: "asc" }];
const filters: CrudFilter[] = [];
const url = stringifyTableParams({
pagination,
sorters,
sorter,
filters,
});
expect(url).toMatchSnapshot();
});
it("parse table params with advanced query object", async () => {
const query = {
current: 1,
pageSize: 10,
sorter: [
{ field: "id", order: "asc" },
{ field: "firstName", order: "desc" },
],
filters: [
{ field: "id", operator: "eq", value: "1" },
{
operator: "or",
value: [
{
field: "age",
operator: "lt",
value: "18",
},
{
field: "age",
operator: "gt",
value: "20",
},
],
},
],
};
const { parsedCurrent, parsedPageSize, parsedSorter, parsedFilters } =
parseTableParamsFromQuery(query);
expect(parsedCurrent).toBe(1);
expect(parsedPageSize).toBe(10);
expect(parsedSorter).toStrictEqual([
{ field: "id", order: "asc" },
{ field: "firstName", order: "desc" },
]);
expect(parsedFilters).toStrictEqual([
{ field: "id", operator: "eq", value: "1" },
{
operator: "or",
value: [
{
field: "age",
operator: "lt",
value: "18",
},
{
field: "age",
operator: "gt",
value: "20",
},
],
},
]);
});
it("unionFilters should override same filters", () => {
expect(
unionFilters(
[
{
field: "foo",
operator: "in",
value: "permenant",
},
],
[
{
field: "foo",
operator: "in",
value: "crud",
},
{
field: "bar",
operator: "in",
value: "crud",
},
],
),
).toMatchInlineSnapshot(`
[
{
"field": "foo",
"operator": "in",
"value": "permenant",
},
{
"field": "bar",
"operator": "in",
"value": "crud",
},
]
`);
});
it("unionFilters should override even when filter value is null but should not keep it in the end result", () => {
expect(
unionFilters(
[],
[
{
field: "foo",
operator: "in",
value: null,
},
{
field: "bar",
operator: "in",
value: undefined,
},
{
field: "baz",
operator: "in",
value: "prev",
},
],
),
).toMatchInlineSnapshot(`
[
{
"field": "baz",
"operator": "in",
"value": "prev",
},
]
`);
});
it("compareFilters filters are the same if their field and operator are the same", () => {
expect(
compareFilters(
{
field: "foo",
operator: "in",
value: "left",
},
{
field: "foo",
operator: "in",
value: "right",
},
),
).toBe(true);
expect(
compareFilters(
{
field: "foo",
operator: "in",
value: "test",
},
{
field: "foo",
operator: "contains",
value: "test",
},
),
).toBe(false);
});
it("compareFilters return correct result when `or` is used", () => {
expect(
compareFilters(
{
operator: "or",
value: [
{
field: "name",
operator: "eq",
value: "test",
},
],
},
{
operator: "or",
value: [
{
field: "created_at",
operator: "gte",
value: "2022-01-01",
},
],
},
),
).toBe(true);
expect(
compareFilters(
{
operator: "or",
value: [
{
field: "name",
operator: "eq",
value: "test",
},
],
},
{
field: "created_at",
operator: "gte",
value: "2022-01-01",
},
),
).toBe(false);
});
it("unionFilters should override `or` filter", () => {
const union = unionFilters(
// permanent filters
[],
// new filters
[
{
field: "other-field",
operator: "in",
value: "crud",
},
{
operator: "or",
value: [
{
field: "created_at",
operator: "contains",
value: "2022",
},
],
},
],
// prev filters
[
{
operator: "or",
value: [
{
field: "name",
operator: "eq",
value: "test",
},
],
},
],
);
// does not include previous `or`
expect(union).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
operator: "or",
value: expect.arrayContaining([
expect.objectContaining({
field: "name",
operator: "eq",
value: "test",
}),
]),
}),
]),
);
// includes new `or` and new filters
expect(union).toMatchObject([
{
field: "other-field",
operator: "in",
value: "crud",
},
{
operator: "or",
value: [
{
field: "created_at",
operator: "contains",
value: "2022",
},
],
},
]);
});
it("unionFilters should remove `or` filter if value is empty array", () => {
const union = unionFilters(
// permanent filters
[],
// new filters
[
{
field: "other-field",
operator: "in",
value: "crud",
},
{
operator: "or",
value: [],
},
],
// prev filters
[
{
operator: "or",
value: [
{
field: "name",
operator: "eq",
value: "test",
},
],
},
],
);
expect(union).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
operator: "or",
}),
]),
);
});
it("unionFilters should keep `or` filter if it's untouched", () => {
const union = unionFilters(
// permanent filters
[],
// new filters
[
{
field: "other-field",
operator: "in",
value: "crud",
},
],
// prev filters
[
{
operator: "or",
value: [
{
field: "name",
operator: "eq",
value: "test",
},
],
},
],
);
expect(union).toEqual(
expect.arrayContaining([
expect.objectContaining({
operator: "or",
}),
]),
);
});
it("unionSorters should override same sorter", () => {
expect(
unionSorters(
[
{
field: "foo",
order: "asc",
},
],
[
{
field: "foo",
order: "asc",
},
{
field: "bar",
order: "asc",
},
],
),
).toMatchInlineSnapshot(`
[
{
"field": "foo",
"order": "asc",
},
{
"field": "bar",
"order": "asc",
},
]
`);
});
it("unionSorters should override even when sorter value is null but should not keep it in the end result", () => {
expect(
unionSorters(
[],
[
{
field: "bar",
order: "asc",
},
{
field: "foo",
order: "asc",
},
],
),
).toMatchInlineSnapshot(`
[
{
"field": "bar",
"order": "asc",
},
{
"field": "foo",
"order": "asc",
},
]
`);
});
it("compareSorters sorters are the same if their field are the same", () => {
expect(
compareSorters(
{
field: "foo",
order: "asc",
},
{
field: "foo",
order: "asc",
},
),
).toBe(true);
expect(
compareSorters(
{
field: "foo",
order: "asc",
},
{
field: "foo",
order: "desc",
},
),
).toBe(true);
});
it("parseTableParams default sorter and filters", () => {
expect(parseTableParams("?current=1&pageSize=10")).toStrictEqual({
parsedCurrent: 1,
parsedFilters: [],
parsedPageSize: 10,
parsedSorter: [],
});
});
it("stringifyTableParams default pagination", () => {
expect(
stringifyTableParams({
pagination: undefined,
sorters: [],
filters: [],
}),
).toEqual("");
});
});

View File

@@ -0,0 +1,188 @@
import differenceWith from "lodash/differenceWith";
import unionWith from "lodash/unionWith";
import qs, { type IStringifyOptions } from "qs";
import warnOnce from "warn-once";
import { pickNotDeprecated } from "@definitions/helpers";
import type {
CrudFilter,
CrudOperators,
CrudSort,
SortOrder,
} from "../../contexts/data/types";
export const parseTableParams = (url: string) => {
const { current, pageSize, sorter, sorters, filters } = qs.parse(
url.substring(1), // remove first ? character
);
return {
parsedCurrent: current && Number(current),
parsedPageSize: pageSize && Number(pageSize),
parsedSorter: (pickNotDeprecated(sorters, sorter) as CrudSort[]) ?? [],
parsedFilters: (filters as CrudFilter[]) ?? [],
};
};
export const parseTableParamsFromQuery = (params: any) => {
const url = qs.stringify(params);
return parseTableParams(`/${url}`);
};
/**
* @internal This function is used to stringify table params from the useTable hook.
*/
export const stringifyTableParams = (params: {
pagination?: { current?: number; pageSize?: number };
sorters: CrudSort[];
filters: CrudFilter[];
[key: string]: any;
}): string => {
const options: IStringifyOptions = {
skipNulls: true,
arrayFormat: "indices",
encode: false,
};
const { pagination, sorter, sorters, filters, ...rest } = params;
const queryString = qs.stringify(
{
...rest,
...(pagination ? pagination : {}),
sorters: pickNotDeprecated(sorters, sorter),
filters,
},
options,
);
return queryString;
};
export const compareFilters = (
left: CrudFilter,
right: CrudFilter,
): boolean => {
if (
left.operator !== "and" &&
left.operator !== "or" &&
right.operator !== "and" &&
right.operator !== "or"
) {
return (
("field" in left ? left.field : undefined) ===
("field" in right ? right.field : undefined) &&
left.operator === right.operator
);
}
return (
("key" in left ? left.key : undefined) ===
("key" in right ? right.key : undefined) &&
left.operator === right.operator
);
};
export const compareSorters = (left: CrudSort, right: CrudSort): boolean =>
left.field === right.field;
// Keep only one CrudFilter per type according to compareFilters
// Items in the array that is passed first to unionWith have higher priority
// CrudFilter items with undefined values are necessary to signify no filter
// After union, don't keep CrudFilter items with undefined value in the result
// Items in the arrays with higher priority are put at the end.
export const unionFilters = (
permanentFilter: CrudFilter[],
newFilters: CrudFilter[],
prevFilters: CrudFilter[] = [],
): CrudFilter[] => {
const isKeyRequired = newFilters.filter(
(f) => (f.operator === "or" || f.operator === "and") && !f.key,
);
if (isKeyRequired.length > 1) {
warnOnce(
true,
"[conditionalFilters]: You have created multiple Conditional Filters at the top level, this requires the key parameter. \nFor more information, see https://refine.dev/docs/advanced-tutorials/data-provider/handling-filters/#top-level-multiple-conditional-filters-usage",
);
}
return unionWith(
permanentFilter,
newFilters,
prevFilters,
compareFilters,
).filter(
(crudFilter) =>
crudFilter.value !== undefined &&
crudFilter.value !== null &&
(crudFilter.operator !== "or" ||
(crudFilter.operator === "or" && crudFilter.value.length !== 0)) &&
(crudFilter.operator !== "and" ||
(crudFilter.operator === "and" && crudFilter.value.length !== 0)),
);
};
export const unionSorters = (
permanentSorter: CrudSort[],
newSorters: CrudSort[],
): CrudSort[] =>
unionWith(permanentSorter, newSorters, compareSorters).filter(
(crudSorter) => crudSorter.order !== undefined && crudSorter.order !== null,
);
// Prioritize filters in the permanentFilter and put it at the end of result array
export const setInitialFilters = (
permanentFilter: CrudFilter[],
defaultFilter: CrudFilter[],
): CrudFilter[] => [
...differenceWith(defaultFilter, permanentFilter, compareFilters),
...permanentFilter,
];
export const setInitialSorters = (
permanentSorter: CrudSort[],
defaultSorter: CrudSort[],
): CrudSort[] => [
...differenceWith(defaultSorter, permanentSorter, compareSorters),
...permanentSorter,
];
export const getDefaultSortOrder = (
columnName: string,
sorter?: CrudSort[],
): SortOrder | undefined => {
if (!sorter) {
return undefined;
}
const sortItem = sorter.find((item) => item.field === columnName);
if (sortItem) {
return sortItem.order as SortOrder;
}
return undefined;
};
export const getDefaultFilter = (
columnName: string,
filters?: CrudFilter[],
operatorType: CrudOperators = "eq",
): CrudFilter["value"] | undefined => {
const filter = filters?.find((filter) => {
if (
filter.operator !== "or" &&
filter.operator !== "and" &&
"field" in filter
) {
const { operator, field } = filter;
return field === columnName && operator === operatorType;
}
return undefined;
});
if (filter) {
return filter.value || [];
}
return undefined;
};