diff --git a/.changeset/spicy-flowers-allow.md b/.changeset/spicy-flowers-allow.md new file mode 100644 index 000000000000..a280a1ca8142 --- /dev/null +++ b/.changeset/spicy-flowers-allow.md @@ -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) diff --git a/packages/core/src/definitions/table/index.ts b/packages/core/src/definitions/table/index.ts index 1fd8eba3267a..d9ce921f03d5 100644 --- a/packages/core/src/definitions/table/index.ts +++ b/packages/core/src/definitions/table/index.ts @@ -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; +}; diff --git a/packages/core/src/hooks/useTable/index.ts b/packages/core/src/hooks/useTable/index.ts index b679e5392468..9d2eec4bf676 100644 --- a/packages/core/src/hooks/useTable/index.ts +++ b/packages/core/src/hooks/useTable/index.ts @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import type { QueryObserverResult, @@ -11,6 +11,9 @@ import warnOnce from "warn-once"; import { pickNotDeprecated } from "@definitions/helpers"; import { + isEqualFilters, + mergeFilters, + mergeSorters, parseTableParams, setInitialFilters, setInitialSorters, @@ -385,6 +388,9 @@ export function useTable< const [current, setCurrent] = useState(defaultCurrent); const [pageSize, setPageSize] = useState(defaultPageSize); + const [urlUpdated, setUrlUpdated] = useState(false); + const lastSyncedUrlFilters = useRef(); + const getCurrentQueryParams = (): object => { if (routerType === "new") { // We get QueryString parameters that are uncontrolled by refine. @@ -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({ resource: identifier, hasPagination,