Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -196,6 +199,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 @@ -384,6 +392,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 @@ -495,9 +506,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