Skip to content

Commit

Permalink
✨ Add custom matchers to client side filters (#1922)
Browse files Browse the repository at this point in the history
Before this PR, all values were matched in the same way:
1. value needs to be string or have string representation via
   getItemValue
2. positive match is returned if (lowercased) value contains
   (lowercased) filter value.

After this PR, a custom algorithm can be provided using "matcher"
property. Direct motivation is DateRange filter which stores filter
values as ISO 8601 time intervals i.e. "2024-04-01/2024-05-01".
Positive match (value is in the range) requires parsing to date objects.

Reference-Url: https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
Reference-Url: #1913

Signed-off-by: Radoslaw Szwajkowski <[email protected]>
  • Loading branch information
rszwajko authored May 29, 2024
1 parent 3949530 commit 3f15491
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 17 deletions.
2 changes: 2 additions & 0 deletions client/src/app/components/FilterToolbar/FilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface IBasicFilterCategory<
* Defaults to using the UI state's value if omitted.
*/
getServerFilterValue?: (filterValue: FilterValue) => string[] | undefined;
/** For client side filtering, provide custom algorithm for testing if the value of `TItem` matches the filter value. */
matcher?: (filter: string, item: TItem) => boolean;
}

export interface IMultiselectFilterCategory<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
FilterCategory,
FilterValue,
getFilterLogicOperator,
} from "@app/components/FilterToolbar";
import { objectKeys } from "@app/utils/utils";
import { IFilterState } from "./useFilterState";

/**
Expand Down Expand Up @@ -44,26 +44,33 @@ export const getLocalFilterDerivedState = <
filterState: { filterValues },
}: ILocalFilterDerivedStateArgs<TItem, TFilterCategoryKey>) => {
const filteredItems = items.filter((item) =>
objectKeys(filterValues).every((categoryKey) => {
const values = filterValues[categoryKey];
Object.entries<FilterValue>(filterValues).every(([filterKey, values]) => {
if (!values || values.length === 0) return true;
const filterCategory = filterCategories.find(
(category) => category.categoryKey === categoryKey
);
let itemValue = (item as any)[categoryKey];
if (filterCategory?.getItemValue) {
itemValue = filterCategory.getItemValue(item);
}
const logicOperator = getFilterLogicOperator(filterCategory);
return values[logicOperator === "AND" ? "every" : "some"](
(filterValue) => {
if (!itemValue) return false;
const lowerCaseItemValue = String(itemValue).toLowerCase();
const lowerCaseFilterValue = String(filterValue).toLowerCase();
return lowerCaseItemValue.indexOf(lowerCaseFilterValue) !== -1;
}
(category) => category.categoryKey === filterKey
);
const defaultMatcher = (filterValue: string, item: TItem) =>
legacyMatcher(
filterValue,
filterCategory?.getItemValue?.(item) ?? (item as any)[filterKey]
);
const matcher = filterCategory?.matcher ?? defaultMatcher;
const logicOperator =
getFilterLogicOperator(filterCategory) === "AND" ? "every" : "some";
return values[logicOperator]((filterValue) => matcher(filterValue, item));
})
);

return { filteredItems };
};

/**
*
* @returns false for any falsy value (regardless of the filter value), true if (coerced to string) lowercased value contains lowercased filter value.
*/
const legacyMatcher = (filterValue: string, value: any) => {
if (!value) return false;
const lowerCaseItemValue = String(value).toLowerCase();
const lowerCaseFilterValue = String(filterValue).toLowerCase();
return lowerCaseItemValue.indexOf(lowerCaseFilterValue) !== -1;
};

0 comments on commit 3f15491

Please sign in to comment.