From 893368dadfc0b753017ade2a0950d4c74d52e482 Mon Sep 17 00:00:00 2001 From: nmanu1 <88398086+nmanu1@users.noreply.github.com> Date: Fri, 11 Nov 2022 16:14:47 -0500 Subject: [PATCH] Add onSelect prop to FilterSearch (#323) Add an `onSelect` prop to `FilterSearch` and deprecate `searchOnSelect`. If an `onSelect` function is passed in, the default selecting (and searching) logic is bypassed and we call the `onSelect`. If `searchOnSelect=true` is passed with an `onSelect`, we log a console warning that the former will be ignored. To allow for the UCSD use case mentioned in the spec, `currentFilter` was updated to allow any `StaticFilter`, not just a field value filter. J=SLAP-2431 TEST=auto, manual See that the added Jest tests pass. In the test-site, pass in an `onSelect` function that matches the current functionality with `searchOnSelect` as either true or false. Also, mimic a UCSD-like use case with compound filters and see that the component behaves correctly when selecting and un-selecting a filter. If both an `onSelect` and `searchOnSelect=true` are passed, see that a console warning is logged. --- docs/search-ui-react.filtersearch.md | 4 +- docs/search-ui-react.filtersearchprops.md | 1 + ...rch-ui-react.filtersearchprops.onselect.md | 13 +++ ...-react.filtersearchprops.searchonselect.md | 5 + docs/search-ui-react.md | 3 +- ...h-ui-react.onselectparams.currentfilter.md | 13 +++ ...eact.onselectparams.executefiltersearch.md | 13 +++ docs/search-ui-react.onselectparams.md | 24 +++++ ...-ui-react.onselectparams.newdisplayname.md | 13 +++ ...earch-ui-react.onselectparams.newfilter.md | 13 +++ ...i-react.onselectparams.setcurrentfilter.md | 13 +++ etc/search-ui-react.api.md | 16 ++- src/components/FilterSearch.tsx | 102 +++++++++++------- src/components/index.ts | 3 +- src/utils/filterutils.tsx | 22 +++- tests/components/FilterSearch.test.tsx | 53 ++++++++- 16 files changed, 264 insertions(+), 47 deletions(-) create mode 100644 docs/search-ui-react.filtersearchprops.onselect.md create mode 100644 docs/search-ui-react.onselectparams.currentfilter.md create mode 100644 docs/search-ui-react.onselectparams.executefiltersearch.md create mode 100644 docs/search-ui-react.onselectparams.md create mode 100644 docs/search-ui-react.onselectparams.newdisplayname.md create mode 100644 docs/search-ui-react.onselectparams.newfilter.md create mode 100644 docs/search-ui-react.onselectparams.setcurrentfilter.md diff --git a/docs/search-ui-react.filtersearch.md b/docs/search-ui-react.filtersearch.md index 2e61f2a74..7fa93c8d2 100644 --- a/docs/search-ui-react.filtersearch.md +++ b/docs/search-ui-react.filtersearch.md @@ -9,14 +9,14 @@ A component which allows a user to search for filters associated with specific e Signature: ```typescript -export declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; +export declare function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | | +| { searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses } | [FilterSearchProps](./search-ui-react.filtersearchprops.md) | | Returns: diff --git a/docs/search-ui-react.filtersearchprops.md b/docs/search-ui-react.filtersearchprops.md index 15cfe8ec6..d7d9bcb18 100644 --- a/docs/search-ui-react.filtersearchprops.md +++ b/docs/search-ui-react.filtersearchprops.md @@ -18,6 +18,7 @@ export interface FilterSearchProps | --- | --- | --- | | [customCssClasses?](./search-ui-react.filtersearchprops.customcssclasses.md) | [FilterSearchCssClasses](./search-ui-react.filtersearchcssclasses.md) | (Optional) CSS classes for customizing the component styling. | | [label?](./search-ui-react.filtersearchprops.label.md) | string | (Optional) The display label for the component. | +| [onSelect?](./search-ui-react.filtersearchprops.onselect.md) | (params: [OnSelectParams](./search-ui-react.onselectparams.md)) => void | (Optional) A function which is called when a filter is selected. | | [placeholder?](./search-ui-react.filtersearchprops.placeholder.md) | string | (Optional) The search input's placeholder text when no text has been entered by the user. Defaults to "Search here...". | | [searchFields](./search-ui-react.filtersearchprops.searchfields.md) | Omit<SearchParameterField, 'fetchEntities'>\[\] | An array of fieldApiName and entityType which indicates what to perform the filter search against. | | [searchOnSelect?](./search-ui-react.filtersearchprops.searchonselect.md) | boolean | (Optional) Whether to trigger a search when an option is selected. Defaults to false. | diff --git a/docs/search-ui-react.filtersearchprops.onselect.md b/docs/search-ui-react.filtersearchprops.onselect.md new file mode 100644 index 000000000..9a6ce464c --- /dev/null +++ b/docs/search-ui-react.filtersearchprops.onselect.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [FilterSearchProps](./search-ui-react.filtersearchprops.md) > [onSelect](./search-ui-react.filtersearchprops.onselect.md) + +## FilterSearchProps.onSelect property + +A function which is called when a filter is selected. + +Signature: + +```typescript +onSelect?: (params: OnSelectParams) => void; +``` diff --git a/docs/search-ui-react.filtersearchprops.searchonselect.md b/docs/search-ui-react.filtersearchprops.searchonselect.md index c6506c3f2..39ec94a54 100644 --- a/docs/search-ui-react.filtersearchprops.searchonselect.md +++ b/docs/search-ui-react.filtersearchprops.searchonselect.md @@ -4,6 +4,11 @@ ## FilterSearchProps.searchOnSelect property +> Warning: This API is now obsolete. +> +> Use the `onSelect` prop instead. +> + Whether to trigger a search when an option is selected. Defaults to false. Signature: diff --git a/docs/search-ui-react.md b/docs/search-ui-react.md index 110b5fde3..e11f7d74e 100644 --- a/docs/search-ui-react.md +++ b/docs/search-ui-react.md @@ -17,7 +17,7 @@ | [executeAutocomplete(searchActions)](./search-ui-react.executeautocomplete.md) | Executes a universal/vertical autocomplete search and return the corresponding response. | | [executeSearch(searchActions)](./search-ui-react.executesearch.md) | Executes a universal/vertical search. | | [FilterDivider({ className })](./search-ui-react.filterdivider.md) | A divider component used to separate NumericalFacets, HierarchicalFacets, StandardFacets, and StaticFilters. | -| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. | +| [FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses })](./search-ui-react.filtersearch.md) | A component which allows a user to search for filters associated with specific entities and fields. | | [getSearchIntents(searchActions)](./search-ui-react.getsearchintents.md) | Get search intents of the current query stored in headless using autocomplete request. | | [getUserLocation(geolocationOptions)](./search-ui-react.getuserlocation.md) | Retrieves user's location using navigator.geolocation API. | | [HierarchicalFacets({ searchOnChange, collapsible, defaultExpanded, includedFieldIds, customCssClasses, delimiter, showMoreLimit })](./search-ui-react.hierarchicalfacets.md) | A component that displays hierarchical facets, in a tree level structure, applicable to the current vertical search. | @@ -70,6 +70,7 @@ | [LocationBiasProps](./search-ui-react.locationbiasprops.md) | The props for the [LocationBias()](./search-ui-react.locationbias.md) component. | | [NumericalFacetsCssClasses](./search-ui-react.numericalfacetscssclasses.md) | The CSS class interface for [NumericalFacets()](./search-ui-react.numericalfacets.md). | | [NumericalFacetsProps](./search-ui-react.numericalfacetsprops.md) | Props for the [NumericalFacets()](./search-ui-react.numericalfacets.md) component. | +| [OnSelectParams](./search-ui-react.onselectparams.md) | The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md). | | [PaginationCssClasses](./search-ui-react.paginationcssclasses.md) | The CSS classes used for pagination. | | [PaginationProps](./search-ui-react.paginationprops.md) | Props for [Pagination()](./search-ui-react.pagination.md) component | | [RangeInputCssClasses](./search-ui-react.rangeinputcssclasses.md) | The CSS class interface for RangeInput. | diff --git a/docs/search-ui-react.onselectparams.currentfilter.md b/docs/search-ui-react.onselectparams.currentfilter.md new file mode 100644 index 000000000..e72f10ff1 --- /dev/null +++ b/docs/search-ui-react.onselectparams.currentfilter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [currentFilter](./search-ui-react.onselectparams.currentfilter.md) + +## OnSelectParams.currentFilter property + +The previously selected filter. + +Signature: + +```typescript +currentFilter: StaticFilter | undefined; +``` diff --git a/docs/search-ui-react.onselectparams.executefiltersearch.md b/docs/search-ui-react.onselectparams.executefiltersearch.md new file mode 100644 index 000000000..ac7720e64 --- /dev/null +++ b/docs/search-ui-react.onselectparams.executefiltersearch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [executeFilterSearch](./search-ui-react.onselectparams.executefiltersearch.md) + +## OnSelectParams.executeFilterSearch property + +A function that executes a filter search and updates the input and dropdown options with the response. + +Signature: + +```typescript +executeFilterSearch: (query?: string) => Promise; +``` diff --git a/docs/search-ui-react.onselectparams.md b/docs/search-ui-react.onselectparams.md new file mode 100644 index 000000000..d2bc68290 --- /dev/null +++ b/docs/search-ui-react.onselectparams.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) + +## OnSelectParams interface + +The parameters that are passed into [FilterSearchProps.onSelect](./search-ui-react.filtersearchprops.onselect.md). + +Signature: + +```typescript +export interface OnSelectParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [currentFilter](./search-ui-react.onselectparams.currentfilter.md) | StaticFilter \| undefined | The previously selected filter. | +| [executeFilterSearch](./search-ui-react.onselectparams.executefiltersearch.md) | (query?: string) => Promise<FilterSearchResponse \| undefined> | A function that executes a filter search and updates the input and dropdown options with the response. | +| [newDisplayName](./search-ui-react.onselectparams.newdisplayname.md) | string | The display name of the newly selected filter. | +| [newFilter](./search-ui-react.onselectparams.newfilter.md) | FieldValueStaticFilter | The newly selected filter. | +| [setCurrentFilter](./search-ui-react.onselectparams.setcurrentfilter.md) | (filter: StaticFilter) => void | A function that sets which filter the component is currently associated with. | + diff --git a/docs/search-ui-react.onselectparams.newdisplayname.md b/docs/search-ui-react.onselectparams.newdisplayname.md new file mode 100644 index 000000000..f47f95d6a --- /dev/null +++ b/docs/search-ui-react.onselectparams.newdisplayname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [newDisplayName](./search-ui-react.onselectparams.newdisplayname.md) + +## OnSelectParams.newDisplayName property + +The display name of the newly selected filter. + +Signature: + +```typescript +newDisplayName: string; +``` diff --git a/docs/search-ui-react.onselectparams.newfilter.md b/docs/search-ui-react.onselectparams.newfilter.md new file mode 100644 index 000000000..ff9ddae77 --- /dev/null +++ b/docs/search-ui-react.onselectparams.newfilter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [newFilter](./search-ui-react.onselectparams.newfilter.md) + +## OnSelectParams.newFilter property + +The newly selected filter. + +Signature: + +```typescript +newFilter: FieldValueStaticFilter; +``` diff --git a/docs/search-ui-react.onselectparams.setcurrentfilter.md b/docs/search-ui-react.onselectparams.setcurrentfilter.md new file mode 100644 index 000000000..90356f104 --- /dev/null +++ b/docs/search-ui-react.onselectparams.setcurrentfilter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-ui-react](./search-ui-react.md) > [OnSelectParams](./search-ui-react.onselectparams.md) > [setCurrentFilter](./search-ui-react.onselectparams.setcurrentfilter.md) + +## OnSelectParams.setCurrentFilter property + +A function that sets which filter the component is currently associated with. + +Signature: + +```typescript +setCurrentFilter: (filter: StaticFilter) => void; +``` diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md index c8352b55a..58a17225f 100644 --- a/etc/search-ui-react.api.md +++ b/etc/search-ui-react.api.md @@ -10,6 +10,8 @@ import { AnalyticsConfig } from '@yext/analytics'; import { AnalyticsService } from '@yext/analytics'; import { AutocompleteResponse } from '@yext/search-headless-react'; import { DirectAnswer as DirectAnswer_2 } from '@yext/search-headless-react'; +import { FieldValueStaticFilter } from '@yext/search-headless-react'; +import { FilterSearchResponse } from '@yext/search-headless-react'; import { HighlightedValue } from '@yext/search-headless-react'; import { Matcher } from '@yext/search-headless-react'; import { NumberRangeValue } from '@yext/search-headless-react'; @@ -20,6 +22,7 @@ import { SearchActions } from '@yext/search-headless-react'; import { SearchHeadless } from '@yext/search-headless-react'; import { SearchIntent } from '@yext/search-headless-react'; import { SearchParameterField } from '@yext/search-headless-react'; +import { StaticFilter } from '@yext/search-headless-react'; import { UniversalLimit } from '@yext/search-headless-react'; import { UnknownFieldValueDirectAnswer } from '@yext/search-headless-react'; import { VerticalResults as VerticalResults_2 } from '@yext/search-headless-react'; @@ -230,7 +233,7 @@ export interface FilterOptionConfig { } // @public -export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; +export function FilterSearch({ searchFields, label, placeholder, searchOnSelect, onSelect, sectioned, customCssClasses }: FilterSearchProps): JSX.Element; // @public export interface FilterSearchCssClasses extends AutocompleteResultCssClasses { @@ -252,8 +255,10 @@ export interface FilterSearchCssClasses extends AutocompleteResultCssClasses { export interface FilterSearchProps { customCssClasses?: FilterSearchCssClasses; label?: string; + onSelect?: (params: OnSelectParams) => void; placeholder?: string; searchFields: Omit[]; + // @deprecated searchOnSelect?: boolean; sectioned?: boolean; } @@ -365,6 +370,15 @@ export type onSearchFunc = (searchEventData: { query?: string; }) => void; +// @public +export interface OnSelectParams { + currentFilter: StaticFilter | undefined; + executeFilterSearch: (query?: string) => Promise; + newDisplayName: string; + newFilter: FieldValueStaticFilter; + setCurrentFilter: (filter: StaticFilter) => void; +} + // @public export function Pagination(props: PaginationProps): JSX.Element | null; diff --git a/src/components/FilterSearch.tsx b/src/components/FilterSearch.tsx index 58dda14f4..db5f8e81c 100644 --- a/src/components/FilterSearch.tsx +++ b/src/components/FilterSearch.tsx @@ -1,9 +1,9 @@ -import { AutocompleteResult, FieldValueStaticFilter, FilterSearchResponse, SearchParameterField, useSearchActions, useSearchState } from '@yext/search-headless-react'; +import { AutocompleteResult, FieldValueStaticFilter, FilterSearchResponse, SearchParameterField, StaticFilter, useSearchActions, useSearchState } from '@yext/search-headless-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useComposedCssClasses } from '../hooks/useComposedCssClasses'; import { useSynchronizedRequest } from '../hooks/useSynchronizedRequest'; import { executeSearch } from '../utils'; -import { getSelectableFieldValueFilters, isDuplicateFieldValueFilter } from '../utils/filterutils'; +import { isDuplicateStaticFilter } from '../utils/filterutils'; import { Dropdown } from './Dropdown/Dropdown'; import { DropdownInput } from './Dropdown/DropdownInput'; import { DropdownItem } from './Dropdown/DropdownItem'; @@ -34,6 +34,27 @@ const builtInCssClasses: Readonly = { option: 'text-sm text-neutral-dark py-1 cursor-pointer hover:bg-gray-100 px-4' }; +/** + * The parameters that are passed into {@link FilterSearchProps.onSelect}. + * + * @public + */ +export interface OnSelectParams { + /** The newly selected filter. */ + newFilter: FieldValueStaticFilter, + /** The display name of the newly selected filter. */ + newDisplayName: string, + /** The previously selected filter. */ + currentFilter: StaticFilter | undefined, + /** A function that sets which filter the component is currently associated with. */ + setCurrentFilter: (filter: StaticFilter) => void, + /** + * A function that executes a filter search and updates the input and dropdown options + * with the response. + */ + executeFilterSearch: (query?: string) => Promise +} + /** * The props for the {@link FilterSearch} component. * @@ -49,8 +70,14 @@ export interface FilterSearchProps { * Defaults to "Search here...". */ placeholder?: string, - /** Whether to trigger a search when an option is selected. Defaults to false. */ + /** + * Whether to trigger a search when an option is selected. Defaults to false. + * + * @deprecated Use the `onSelect` prop instead. + */ searchOnSelect?: boolean, + /** A function which is called when a filter is selected. */ + onSelect?: (params: OnSelectParams) => void, /** Determines whether or not the results of the filter search are separated by field. Defaults to false. */ sectioned?: boolean, /** CSS classes for customizing the component styling. */ @@ -70,6 +97,7 @@ export function FilterSearch({ label, placeholder = 'Search here...', searchOnSelect, + onSelect, sectioned = false, customCssClasses }: FilterSearchProps): JSX.Element { @@ -78,13 +106,9 @@ export function FilterSearch({ return { ...searchField, fetchEntities: false }; }); const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses); - const [currentFilter, setCurrentFilter] = useState(); + const [currentFilter, setCurrentFilter] = useState(); const [filterQuery, setFilterQuery] = useState(); const staticFilters = useSearchState(state => state.filters.static); - const fieldValueFilters = useMemo( - () => getSelectableFieldValueFilters(staticFilters ?? []), - [staticFilters] - ); const [ filterSearchResponse, @@ -99,14 +123,14 @@ export function FilterSearch({ ); useEffect(() => { - if (currentFilter && fieldValueFilters?.find(f => - isDuplicateFieldValueFilter(f, currentFilter) && !f.selected + if (currentFilter && staticFilters?.find(f => + isDuplicateStaticFilter(f.filter, currentFilter) && !f.selected )) { clearFilterSearchResponse(); setCurrentFilter(undefined); setFilterQuery(''); } - }, [clearFilterSearchResponse, currentFilter, fieldValueFilters]); + }, [clearFilterSearchResponse, currentFilter, staticFilters]); const sections = useMemo(() => { return filterSearchResponse?.sections.filter(section => section.results.length > 0) ?? []; @@ -114,41 +138,40 @@ export function FilterSearch({ const hasResults = sections.flatMap(s => s.results).length > 0; - const handleDropdownEvent = useCallback((value, itemData, select) => { + const handleSelectDropdown = useCallback((_value, _index, itemData) => { const newFilter = itemData?.filter as FieldValueStaticFilter; const newDisplayName = itemData?.displayName as string; - if (newFilter && newDisplayName) { - if (select) { - if (currentFilter) { - searchActions.setFilterOption({ filter: currentFilter, selected: false }); - } - searchActions.setFilterOption({ filter: newFilter, displayName: newDisplayName, selected: true - }); - setCurrentFilter(newFilter); - setFilterQuery(newDisplayName); - executeFilterSearch(newDisplayName); - if (searchOnSelect) { - searchActions.setOffset(0); - searchActions.resetFacets(); - executeSearch(searchActions); - } - } else { - setFilterQuery(value); - executeFilterSearch(value); + if (!newFilter || !newDisplayName) { + return; + } + + if (onSelect) { + if (searchOnSelect) { + console.warn('Both searchOnSelect and onSelect props were passed to the component.' + + ' Using onSelect instead of searchOnSelect as the latter is deprecated.'); } + return onSelect({ + newFilter, + newDisplayName, + currentFilter, + setCurrentFilter, + executeFilterSearch + }); } - }, [currentFilter, searchActions, executeFilterSearch, searchOnSelect]); - const handleSelectDropdown = useCallback((value, _index, itemData) => { - handleDropdownEvent(value, itemData, true); - }, [handleDropdownEvent]); + if (currentFilter) { + searchActions.setFilterOption({ filter: currentFilter, selected: false }); + } + searchActions.setFilterOption({ filter: newFilter, displayName: newDisplayName, selected: true }); + setCurrentFilter(newFilter); + executeFilterSearch(newDisplayName); - const handleToggleDropdown = - useCallback((isActive, _prevValue, value, _index, itemData) => { - if (!isActive) { - handleDropdownEvent(value, itemData, false); + if (searchOnSelect) { + searchActions.setOffset(0); + searchActions.resetFacets(); + executeSearch(searchActions); } - }, [handleDropdownEvent]); + }, [currentFilter, searchActions, executeFilterSearch, onSelect, searchOnSelect]); const meetsSubmitCritera = useCallback(index => index >= 0, []); @@ -199,7 +222,6 @@ export function FilterSearch({ diff --git a/src/components/index.ts b/src/components/index.ts index 48b32ea48..19d29f50a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -35,7 +35,8 @@ export { export { FilterSearch, FilterSearchCssClasses, - FilterSearchProps + FilterSearchProps, + OnSelectParams } from './FilterSearch'; export { diff --git a/src/utils/filterutils.tsx b/src/utils/filterutils.tsx index acbe1c8d6..7347bf8f5 100644 --- a/src/utils/filterutils.tsx +++ b/src/utils/filterutils.tsx @@ -1,4 +1,4 @@ -import { NearFilterValue, FieldValueFilter, NumberRangeValue, Matcher, SearchActions, DisplayableFacet, SelectableStaticFilter } from '@yext/search-headless-react'; +import { NearFilterValue, FieldValueFilter, NumberRangeValue, Matcher, SearchActions, DisplayableFacet, SelectableStaticFilter, StaticFilter } from '@yext/search-headless-react'; import { isEqual } from 'lodash'; import { isNumberRangeFilter } from '../models/NumberRangeFilter'; import { SelectableFieldValueFilter } from '../models/SelectableFieldValueFilter'; @@ -43,6 +43,26 @@ export function isDuplicateFieldValueFilter( return true; } +/** + * Returns true if the two given static filters are the same. + */ +export function isDuplicateStaticFilter(thisFilter: StaticFilter, otherFilter: StaticFilter): boolean { + if (thisFilter.kind === 'fieldValue') { + return otherFilter.kind === 'fieldValue' + ? isDuplicateFieldValueFilter(thisFilter, otherFilter) + : false; + } + + if (otherFilter.kind === 'fieldValue') { + return false; + } + + return thisFilter.combinator === otherFilter.combinator + && thisFilter.filters.length === otherFilter.filters.length + && thisFilter.filters.every(t => otherFilter.filters.some(o => isDuplicateStaticFilter(t, o))) + && otherFilter.filters.every(o => thisFilter.filters.some(t => isDuplicateStaticFilter(o, t))); +} + /** * Finds the {@link SelectableFieldValueFilter} from the list provided that matches * the given {@link FieldValueFilter}. If no matching {@link SelectableFieldValueFilter} diff --git a/tests/components/FilterSearch.test.tsx b/tests/components/FilterSearch.test.tsx index 59e1afdbb..56da0cc95 100644 --- a/tests/components/FilterSearch.test.tsx +++ b/tests/components/FilterSearch.test.tsx @@ -179,6 +179,26 @@ describe('search with section labels', () => { }); }); + it('executes onSelect function when a filter is selected', async () => { + const mockedOnSelect = jest.fn(); + const setFilterOption = jest.spyOn(SearchHeadless.prototype, 'setFilterOption'); + const executeFilterSearch = jest + .spyOn(SearchHeadless.prototype, 'executeFilterSearch') + .mockResolvedValue(labeledFilterSearchResponse); + renderFilterSearch({ searchFields: searchFieldsProp, onSelect: mockedOnSelect }); + const searchBarElement = screen.getByRole('textbox'); + + userEvent.type(searchBarElement, 'n'); + await waitFor(() => screen.findByText('first name 1')); + + userEvent.type(searchBarElement, '{enter}'); + await waitFor(() => { + expect(executeFilterSearch).toHaveBeenCalled(); + }); + expect(mockedOnSelect).toBeCalled(); + expect(setFilterOption).not.toBeCalled(); + }); + describe('searchOnSelect = true', () => { it('triggers a search on pressing "enter" when an autocomplete result is selected', async () => { const mockExecuteSearch = jest.spyOn(searchOperations, 'executeSearch'); @@ -281,6 +301,37 @@ describe('search with section labels', () => { expect(setOffsetCallOrder).toBeLessThan(mockExecuteSearchCallOrder); expect(resetFacetsCallOrder).toBeLessThan(mockExecuteSearchCallOrder); }); + + describe('onSelect prop is passed', () => { + it('ignores searchOnSelect, gives a warning, and calls onSelect when a filter is selected', async () => { + const consoleWarnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); + const mockedOnSelect = jest.fn(); + const setFilterOption = jest.spyOn(SearchHeadless.prototype, 'setFilterOption'); + const mockExecuteSearch = jest.spyOn(searchOperations, 'executeSearch'); + const executeFilterSearch = jest + .spyOn(SearchHeadless.prototype, 'executeFilterSearch') + .mockResolvedValue(labeledFilterSearchResponse); + renderFilterSearch({ + searchFields: searchFieldsProp, + searchOnSelect: true, + onSelect: mockedOnSelect + }); + const searchBarElement = screen.getByRole('textbox'); + + userEvent.type(searchBarElement, 'n'); + await waitFor(() => screen.findByText('first name 1')); + + userEvent.type(searchBarElement, '{enter}'); + await waitFor(() => { + expect(executeFilterSearch).toHaveBeenCalled(); + }); + expect(mockedOnSelect).toBeCalled(); + expect(setFilterOption).not.toBeCalled(); + expect(mockExecuteSearch).not.toBeCalled(); + expect(consoleWarnSpy).toBeCalledWith('Both searchOnSelect and onSelect props were passed to the component.' + + ' Using onSelect instead of searchOnSelect as the latter is deprecated.'); + }); + }); }); describe('searchOnSelect = false', () => { @@ -507,4 +558,4 @@ it('toggling the dropdown does not change selected filters', async () => { userEvent.click(externalDiv); expect(setFilterOption).toBeCalledTimes(1); -}); \ No newline at end of file +});