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): sync url params with internal state for useTable hook #6645

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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)
120 changes: 120 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,123 @@ 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
const isEmptyValue = (value: any) => {
if (value === "") {
return true;
}
if (Array.isArray(value) && value.length === 0) {
return true;
}
if (value === undefined) {
return true;
}
return false;
};

if (isEmptyValue(filter1Value) && isEmptyValue(filter2Value)) {
return true;
}

return filter1Value === filter2Value;
});

return isEqual;
};
89 changes: 88 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 @@ -197,6 +200,11 @@ type SyncWithLocationParams = {
filters: CrudFilter[];
};

type LastUrlSyncParams = {
sorters: CrudSort[];
filters: CrudFilter[];
};

export type useTableReturnType<
TData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
Expand Down Expand Up @@ -385,6 +393,9 @@ export function useTable<
const [current, setCurrent] = useState<number>(defaultCurrent);
const [pageSize, setPageSize] = useState<number>(defaultPageSize);

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

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

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

// update lastSynched url params
useEffect(() => {
if (urlUpdated) {
lastSyncedUrlParams.current = {
filters: differenceWith(filters, preferredPermanentFilters, isEqual),
sorters: differenceWith(sorters, preferredPermanentSorters, isEqual),
};

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

// 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 isInternalSyncWithUrlFilters = isEqualFilters(
// currentFilters,
// lastSyncedUrlParams.current?.filters,
// );
// let newFilters: CrudFilter[] = [];
// const currentSorters = sorters;
// const currentUrlSorters = parsedParams.params?.sorters;
// const initialSorters = setInitialSorters(
// preferredPermanentSorters,
// defaultSorter ?? [],
// );
// const sortersAreEqual = (() => {
// if (currentUrlSorters === undefined && currentSorters.length === 0)
// return true;
// return isEqual(currentUrlSorters, currentSorters);
// })();
// const isInternalSyncWithUrlSorters = isEqual(
// currentSorters,
// lastSyncedUrlParams.current?.sorters,
// );
// let newSorters: CrudSort[] = [];
// // if last changes were in sync; i.e: current internal state === last url state
// // &&
// // current url state changed but did not affect current internal state
// if (isInternalSyncWithUrlFilters && !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 (isInternalSyncWithUrlSorters && !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.params?.filters,
filters,
lastSyncedUrlParams.current?.filters,
]);

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