Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): reset table filters and sorters on current page re-navigate via side-nav #6389

Closed
7 changes: 7 additions & 0 deletions .changeset/spicy-flowers-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@refinedev/core": patch
---

Update `useTable` hook to handle case where a user navigates to current page by clicking side-nav link intending to reset the filters and sorters.

[Resolves #6300](https://github.com/refinedev/refine/issues/6300)
110 changes: 110 additions & 0 deletions packages/core/src/definitions/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,113 @@ export const getDefaultFilter = (

return undefined;
};

export const mergeFilters = (
currentUrlFilters: CrudFilter[],
currentFilters: CrudFilter[],
): CrudFilter[] => {
const mergedFilters = currentFilters.map((tableFilter) => {
const matchingURLFilter = currentUrlFilters.find(
(urlFilter) =>
"field" in tableFilter &&
"field" in urlFilter &&
tableFilter.field === urlFilter.field &&
tableFilter.operator === urlFilter.operator,
);

// override current filter wih url filter
if (matchingURLFilter) {
return { ...tableFilter, ...matchingURLFilter };
}

return tableFilter;
});

// add any other URL filters not in the current filters
const additionalURLFilters = currentUrlFilters.filter(
(urlFilter) =>
!currentFilters.some(
(tableFilter) =>
"field" in tableFilter &&
"field" in urlFilter &&
tableFilter.field === urlFilter.field &&
tableFilter.operator === urlFilter.operator,
),
);

return [...mergedFilters, ...additionalURLFilters];
};

export const mergeSorters = (
currentUrlSorters: CrudSort[],
currentSorters: CrudSort[],
): CrudSort[] => {
const merged: CrudSort[] = [...currentUrlSorters];

for (const sorter of currentSorters) {
const exists = merged.some((s) => compareSorters(s, sorter));
if (!exists) {
merged.push(sorter);
}
}

return merged;
};

export const isEqualFilters = (
filter1: CrudFilter[] | undefined,
filter2: CrudFilter[] | undefined,
): boolean => {
if (!filter1 || !filter2) return false;
if (filter1.length !== filter2.length) return false;

const isEqual = filter1.every((f1) => {
// same fields/keys and operators
const isEqualParamsF2 = filter2.find((f2) => compareFilters(f1, f2));

if (!isEqualParamsF2) return false;

const filter1Value = f1.value;
const filter2Value = isEqualParamsF2.value;

// if they both have values, compare
if (filter1Value && filter2Value) {
if (Array.isArray(filter1Value) && Array.isArray(filter2Value)) {
if (filter1Value.length === 0 && filter2Value.length === 0) {
return true;
}

// if array of primitives, compare
if (
filter1Value.every((v) => typeof v !== "object") &&
filter2Value.every((v) => typeof v !== "object")
) {
return (
filter1Value.length === filter2Value.length &&
filter1Value.every((v) => filter2Value.includes(v))
);
}

// recursion because of type def. ConditionalFilter["value"]
return isEqualFilters(filter1Value, filter2Value);
}

// compare primitives (string, number, ...null?)
// because of type def. LogicalFilter["value"]
return filter1Value === filter2Value;
}

// if either is undefined, it means it was initialized,
// so logically equal.
if (
(filter1Value && filter2Value === undefined) ||
(filter1Value === undefined && filter2Value)
) {
return true;
}

return filter1Value === filter2Value;
});

return isEqual;
};
85 changes: 84 additions & 1 deletion packages/core/src/hooks/useTable/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";

import type {
QueryObserverResult,
Expand All @@ -11,6 +11,9 @@ import warnOnce from "warn-once";

import { pickNotDeprecated } from "@definitions/helpers";
import {
isEqualFilters,
mergeFilters,
mergeSorters,
parseTableParams,
setInitialFilters,
setInitialSorters,
Expand Down Expand Up @@ -385,6 +388,9 @@ export function useTable<
const [current, setCurrent] = useState<number>(defaultCurrent);
const [pageSize, setPageSize] = useState<number>(defaultPageSize);

const [urlUpdated, setUrlUpdated] = useState(false);
const lastSyncedUrlFilters = useRef<CrudFilter[] | undefined>();

const getCurrentQueryParams = (): object => {
if (routerType === "new") {
// We get QueryString parameters that are uncontrolled by refine.
Expand Down Expand Up @@ -496,9 +502,86 @@ export function useTable<
shallow: true,
});
}

setUrlUpdated(true);
}
}, [syncWithLocation, current, pageSize, sorters, filters]);

// update lastSynched url filters
useEffect(() => {
if (urlUpdated) {
lastSyncedUrlFilters.current = differenceWith(
filters,
preferredPermanentFilters,
isEqual,
);

// reset
setUrlUpdated(false);
}
}, [urlUpdated, filters]);

// watch URL filters, sorters to update internal filters, sorters
useEffect(() => {
if (syncWithLocation) {
const currentFilters = filters;
const currentUrlFilters = parsedParams?.params?.filters;
const initialFilters = setInitialFilters(
preferredPermanentFilters,
defaultFilter ?? [],
);

const filtersAreEqual = isEqualFilters(currentUrlFilters, currentFilters);
const isInternalSyncWithUrl = isEqualFilters(
currentFilters,
lastSyncedUrlFilters.current,
);
let newFilters: CrudFilter[] = [];

// const currentSorters = sorters;
// const currentUrlSorters = parsedParams.params?.sorters;
// const initialSorters = setInitialSorters(
// preferredPermanentSorters,
// defaultSorter ?? [],
// );

// const sortersAreEqual = isEqual(currentUrlSorters, currentSorters);
// let newSorters: CrudSort[] = [];

// wait for internal state to update url state
if (!isInternalSyncWithUrl) return;

if (!filtersAreEqual) {
// fallback to initial
if (!currentUrlFilters || currentUrlFilters.length === 0) {
newFilters = initialFilters;
} else {
// since they aren't equal, merge the two
newFilters = mergeFilters(currentUrlFilters, currentFilters);
}

setFilters(newFilters);
}

// if (!sortersAreEqual) {
// // fallback to initial
// if (!currentUrlSorters || currentUrlSorters.length === 0) {
// newSorters = initialSorters;
// } else {
// // since they aren't equal, merge the two
// newSorters = mergeSorters(currentUrlSorters, currentSorters);
// }

// setSorters(newSorters);
// }
}
}, [
parsedParams,
filters,
lastSyncedUrlFilters.current,
// sorters,
]);

const queryResult = useList<TQueryFnData, TError, TData>({
resource: identifier,
hasPagination,
Expand Down
Loading