From 21d8259f37b9431c1c758a0bb68956d23cde1e5f Mon Sep 17 00:00:00 2001 From: dmbrooke <38883189+dmbrooke@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:37:40 -0400 Subject: [PATCH] feat(headless): breadcrumb manager facet exclusion (#3043) * First pass https://coveord.atlassian.net/browse/KIT-2563 * UTs and further improving breadcrumb-manager controller https://coveord.atlassian.net/browse/KIT-2563 * Add static filter breadcrumb exclusion support https://coveord.atlassian.net/browse/KIT-2563 * Further improve UTs, support insight breadcrumb manager https://coveord.atlassian.net/browse/KIT-2563 * Remove outdated TODO https://coveord.atlassian.net/browse/KIT-2563 * Lint https://coveord.atlassian.net/browse/KIT-2563 * Move method config to its own type https://coveord.atlassian.net/browse/KIT-2563 --- .../result-children-common.tsx | 2 +- .../headless-breadcrumb-manager.test.ts | 175 ++++++++++-- .../headless-breadcrumb-manager.ts | 93 +++++-- .../headless-core-breadcrumb-manager.ts | 74 +++-- ...eadless-insight-breadcrumb-manager.test.ts | 254 ++++++++++++++---- .../headless-insight-breadcrumb-manager.ts | 99 +++++-- .../date-facet-set/date-facet-selectors.ts | 10 + .../numeric-facet-selectors.ts | 10 + 8 files changed, 564 insertions(+), 153 deletions(-) diff --git a/packages/atomic/src/components/common/result-children/result-children-common.tsx b/packages/atomic/src/components/common/result-children/result-children-common.tsx index f02df82f7c0..02607187454 100644 --- a/packages/atomic/src/components/common/result-children/result-children-common.tsx +++ b/packages/atomic/src/components/common/result-children/result-children-common.tsx @@ -24,7 +24,7 @@ interface ResultChildrenProps { getNoResultText: () => string; getDisplayConfig: () => DisplayConfig; getImageSize: () => ResultDisplayImageSize | undefined; - renderChild: (child: FoldedResult, isLast: boolean) => any; + renderChild: (child: FoldedResult, isLast: boolean) => VNode; setInitialChildren: (initialChildren: FoldedResult[]) => void; toggleShowInitialChildren: () => void; } diff --git a/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.test.ts b/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.test.ts index 15974bee0bc..c23945f5d6d 100644 --- a/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.test.ts +++ b/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.test.ts @@ -4,21 +4,31 @@ import {deselectAllCategoryFacetValues} from '../../features/facets/category-fac import {categoryFacetSetReducer as categoryFacetSet} from '../../features/facets/category-facet-set/category-facet-set-slice'; import {CategoryFacetValue} from '../../features/facets/category-facet-set/interfaces/response'; import { + toggleExcludeFacetValue, toggleSelectFacetValue, updateFreezeCurrentValues, } from '../../features/facets/facet-set/facet-set-actions'; import {facetSetReducer as facetSet} from '../../features/facets/facet-set/facet-set-slice'; import {FacetValue} from '../../features/facets/facet-set/interfaces/response'; -import {toggleSelectDateFacetValue} from '../../features/facets/range-facets/date-facet-set/date-facet-actions'; +import { + toggleExcludeDateFacetValue, + toggleSelectDateFacetValue, +} from '../../features/facets/range-facets/date-facet-set/date-facet-actions'; import {dateFacetSetReducer as dateFacetSet} from '../../features/facets/range-facets/date-facet-set/date-facet-set-slice'; import {DateFacetValue} from '../../features/facets/range-facets/date-facet-set/interfaces/response'; import {NumericFacetValue} from '../../features/facets/range-facets/numeric-facet-set/interfaces/response'; -import {toggleSelectNumericFacetValue} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; +import { + toggleExcludeNumericFacetValue, + toggleSelectNumericFacetValue, +} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; import {numericFacetSetReducer as numericFacetSet} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-slice'; import {executeSearch} from '../../features/search/search-actions'; import {searchReducer as search} from '../../features/search/search-slice'; import {getSearchInitialState} from '../../features/search/search-state'; -import {toggleSelectStaticFilterValue} from '../../features/static-filter-set/static-filter-set-actions'; +import { + toggleExcludeStaticFilterValue, + toggleSelectStaticFilterValue, +} from '../../features/static-filter-set/static-filter-set-actions'; import {SearchAppState} from '../../state/search-app-state'; import { buildMockSearchAppEngine, @@ -146,7 +156,32 @@ describe('headless breadcrumb manager', () => { ); }); - it('dispatches a toggleSelectFacetValue action when #deselectBreadcrumb is called', () => { + it('dispatches an executeSearch action on exclusion', () => { + facetBreadcrumbs[0].values[0].deselect(); + expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); + }); + + it('dispatches an toggleExcludeFacetValue action on exclusion', () => { + facetBreadcrumbs[0].values[0].deselect(); + expect(engine.actions).toContainEqual( + toggleSelectFacetValue({ + facetId, + selection: mockSelectedValue, + }) + ); + }); + + it('dispatches an updateFreezeCurrentValues action on exclusion', () => { + facetBreadcrumbs[0].values[0].deselect(); + expect(engine.actions).toContainEqual( + updateFreezeCurrentValues({ + facetId, + freezeCurrentValues: false, + }) + ); + }); + + it('dispatches a toggleSelectFacetValue action when #deselectBreadcrumb is called for a selected facet value', () => { breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[0]); expect(engine.actions).toContainEqual( toggleSelectFacetValue({ @@ -155,14 +190,26 @@ describe('headless breadcrumb manager', () => { }) ); }); + + it('dispatches a toggleExcludeFacetValue action when #deselectBreadcrumb is called for an excluded facet value', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); }); describe('date facet breadcrumbs', () => { let mockSelectedValue: DateFacetValue; + let mockExcludedValue: DateFacetValue; let facetBreadcrumbs: DateFacetBreadcrumb[]; beforeEach(() => { mockSelectedValue = buildMockDateFacetValue({state: 'selected'}); + mockExcludedValue = buildMockDateFacetValue({state: 'excluded'}); state = createMockState({ search: { @@ -172,7 +219,7 @@ describe('headless breadcrumb manager', () => { facets: [ buildMockDateFacetResponse({ facetId, - values: [mockSelectedValue], + values: [mockSelectedValue, mockExcludedValue], }), ], }, @@ -190,6 +237,7 @@ describe('headless breadcrumb manager', () => { it('#state gets date facet breadcrumbs correctly', () => { expect(facetBreadcrumbs[0].values[0].value).toBe(mockSelectedValue); + expect(facetBreadcrumbs[0].values[1].value).toBe(mockExcludedValue); }); it('dispatches an executeSearch action on selection', () => { @@ -207,7 +255,7 @@ describe('headless breadcrumb manager', () => { ); }); - it('dispatches a toggleSelectDateFacetValue action when #deselectBreadcrumb is called', () => { + it('dispatches a toggleSelectDateFacetValue action when #deselectBreadcrumb is called for a selected facet value', () => { breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[0]); expect(engine.actions).toContainEqual( toggleSelectDateFacetValue({ @@ -216,14 +264,26 @@ describe('headless breadcrumb manager', () => { }) ); }); + + it('dispatches a toggleExcludeDateFacetValue action when #deselectBreadcrumb is called for an excluded facet value', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeDateFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); }); describe('numeric facet breadcrumbs', () => { let mockSelectedValue: NumericFacetValue; + let mockExcludedValue: NumericFacetValue; let facetBreadcrumbs: NumericFacetBreadcrumb[]; beforeEach(() => { mockSelectedValue = buildMockNumericFacetValue({state: 'selected'}); + mockExcludedValue = buildMockNumericFacetValue({state: 'excluded'}); state = createMockState({ search: { @@ -233,7 +293,7 @@ describe('headless breadcrumb manager', () => { facets: [ buildMockNumericFacetResponse({ facetId, - values: [mockSelectedValue], + values: [mockSelectedValue, mockExcludedValue], }), ], }, @@ -251,6 +311,7 @@ describe('headless breadcrumb manager', () => { it('#state gets numeric facet breadcrumbs correctly', () => { expect(facetBreadcrumbs[0].values[0].value).toBe(mockSelectedValue); + expect(facetBreadcrumbs[0].values[1].value).toBe(mockExcludedValue); }); it('dispatches an executeSearch action on selection', () => { @@ -258,17 +319,22 @@ describe('headless breadcrumb manager', () => { expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); }); - it('dispatches a toggleSelectNumericFacetValue action on selection', () => { - facetBreadcrumbs[0].values[0].deselect(); + it('dispatches an executeSearch action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); + }); + + it('dispatches a toggleExcludeNumericFacetValue action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); expect(engine.actions).toContainEqual( - toggleSelectNumericFacetValue({ + toggleExcludeNumericFacetValue({ facetId, - selection: mockSelectedValue, + selection: mockExcludedValue, }) ); }); - it('dispatches a toggleSelectNumericFacetValue action when #deselectBreadcrumb is called', () => { + it('dispatches a toggleSelectNumericFacetValue action when #deselectBreadcrumb is called for a selected facet value', () => { breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[0]); expect(engine.actions).toContainEqual( toggleSelectNumericFacetValue({ @@ -277,6 +343,15 @@ describe('headless breadcrumb manager', () => { }) ); }); + it('dispatches a toggleExcludeNumericFacetValue action when #deselectBreadcrumb is called for an excluded facet value', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeNumericFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); }); describe('category facet breadcrumbs', () => { @@ -339,7 +414,7 @@ describe('headless breadcrumb manager', () => { ); }); - it('dispatches a deselectAllCategoryFacetValues action when #deselectBreadcrumb is called', () => { + it('dispatches a deselectAllCategoryFacetValues action when #deselectBreadcrumb is called for a selected facet value', () => { breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0]); expect(engine.actions).toContainEqual( deselectAllCategoryFacetValues(facetId) @@ -354,10 +429,17 @@ describe('headless breadcrumb manager', () => { caption: 'c', state: 'selected', }); + const excluded = buildMockStaticFilterValue({ + caption: 'd', + state: 'excluded', + }); beforeEach(() => { state.staticFilterSet = { - [id]: buildMockStaticFilterSlice({id, values: [idle, selected]}), + [id]: buildMockStaticFilterSlice({ + id, + values: [idle, selected, excluded], + }), }; }); @@ -369,8 +451,10 @@ describe('headless breadcrumb manager', () => { expect(firstFilter.id).toBe(id); const {values} = firstFilter; - expect(values.length).toBe(1); + expect(values.length).toBe(2); + expect(values[0].value.caption).toBe(selected.caption); + expect(values[1].value.caption).toBe(excluded.caption); }); it('#state.hasBreadcrumbs returns true', () => { @@ -378,24 +462,46 @@ describe('headless breadcrumb manager', () => { }); describe('#deselectBreadcrumb with a static filter breadcrumb value dispatches the correct actions', () => { - beforeEach(() => { - const {staticFilterBreadcrumbs} = breadcrumbManager.state; - const [firstBreadcrumb] = staticFilterBreadcrumbs[0].values; + describe('#selected values', () => { + beforeEach(() => { + const {staticFilterBreadcrumbs} = breadcrumbManager.state; + const [selectedBreadcrumb] = staticFilterBreadcrumbs[0].values; + breadcrumbManager.deselectBreadcrumb(selectedBreadcrumb); + }); - breadcrumbManager.deselectBreadcrumb(firstBreadcrumb); - }); + it('dispatches #toggleSelectStaticFilterValue', () => { + const toggleSelect = toggleSelectStaticFilterValue({ + id, + value: selected, + }); + expect(engine.actions).toContainEqual(toggleSelect); + }); - it('dispatches #toggleSelectStaticFilterValue', () => { - const toggleSelect = toggleSelectStaticFilterValue({ - id, - value: selected, + it('dispatches #executeSearch', () => { + const action = engine.findAsyncAction(executeSearch.pending); + expect(action).toBeTruthy(); }); - expect(engine.actions).toContainEqual(toggleSelect); }); - it('dispatches #executeSearch', () => { - const action = engine.findAsyncAction(executeSearch.pending); - expect(action).toBeTruthy(); + describe('#excluded values', () => { + beforeEach(() => { + const {staticFilterBreadcrumbs} = breadcrumbManager.state; + const [, excludedBreadcrumb] = staticFilterBreadcrumbs[0].values; + breadcrumbManager.deselectBreadcrumb(excludedBreadcrumb); + }); + + it('dispatches #toggleExcludeStaticFilterValue', () => { + const toggleExclude = toggleExcludeStaticFilterValue({ + id, + value: excluded, + }); + expect(engine.actions).toContainEqual(toggleExclude); + }); + + it('dispatches #executeSearch', () => { + const action = engine.findAsyncAction(executeSearch.pending); + expect(action).toBeTruthy(); + }); }); }); }); @@ -411,7 +517,18 @@ describe('headless breadcrumb manager', () => { expect(breadcrumbManager.state.hasBreadcrumbs).toBe(true); }); - it('hasBreadcrumbs returns false when no facet value is selected', () => { + it('hasBreadcrumbs returns true when a facet value is excluded', () => { + state.numericFacetSet[facetId] = buildMockNumericFacetSlice({ + request: buildMockNumericFacetRequest({facetId}), + }); + const mockSelectedValue = buildMockNumericFacetValue({state: 'excluded'}); + state.search.response.facets = [ + buildMockNumericFacetResponse({facetId, values: [mockSelectedValue]}), + ]; + expect(breadcrumbManager.state.hasBreadcrumbs).toBe(true); + }); + + it('hasBreadcrumbs returns false when no facet value is selected or excluded', () => { state.search.response.facets = []; expect(breadcrumbManager.state.hasBreadcrumbs).toBe(false); }); diff --git a/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.ts b/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.ts index 0d959fe4014..29e5dd1ee59 100644 --- a/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.ts +++ b/packages/headless/src/controllers/breadcrumb-manager/headless-breadcrumb-manager.ts @@ -5,25 +5,36 @@ import {logCategoryFacetBreadcrumb} from '../../features/facets/category-facet-s import {categoryFacetResponseSelectedValuesSelector} from '../../features/facets/category-facet-set/category-facet-set-selectors'; import {categoryFacetSetReducer as categoryFacetSet} from '../../features/facets/category-facet-set/category-facet-set-slice'; import { + toggleExcludeFacetValue, toggleSelectFacetValue, updateFreezeCurrentValues, } from '../../features/facets/facet-set/facet-set-actions'; import {logFacetBreadcrumb} from '../../features/facets/facet-set/facet-set-analytics-actions'; import {facetResponseActiveValuesSelector} from '../../features/facets/facet-set/facet-set-selectors'; import {facetSetReducer as facetSet} from '../../features/facets/facet-set/facet-set-slice'; +import {FacetSlice} from '../../features/facets/facet-set/facet-set-state'; import {logClearBreadcrumbs} from '../../features/facets/generic/facet-generic-analytics-actions'; -import {toggleSelectDateFacetValue} from '../../features/facets/range-facets/date-facet-set/date-facet-actions'; +import { + toggleExcludeDateFacetValue, + toggleSelectDateFacetValue, +} from '../../features/facets/range-facets/date-facet-set/date-facet-actions'; import {logDateFacetBreadcrumb} from '../../features/facets/range-facets/date-facet-set/date-facet-analytics-actions'; -import {dateFacetSelectedValuesSelector} from '../../features/facets/range-facets/date-facet-set/date-facet-selectors'; +import {dateFacetActiveValuesSelector} from '../../features/facets/range-facets/date-facet-set/date-facet-selectors'; import {dateFacetSetReducer as dateFacetSet} from '../../features/facets/range-facets/date-facet-set/date-facet-set-slice'; -import {toggleSelectNumericFacetValue} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; +import {DateFacetSlice} from '../../features/facets/range-facets/date-facet-set/date-facet-set-state'; +import { + toggleExcludeNumericFacetValue, + toggleSelectNumericFacetValue, +} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; import {logNumericFacetBreadcrumb} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-analytics-actions'; -import {numericFacetSelectedValuesSelector} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-selectors'; +import {numericFacetActiveValuesSelector} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-selectors'; import {numericFacetSetReducer as numericFacetSet} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-slice'; +import {NumericFacetSlice} from '../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-state'; import {executeSearch} from '../../features/search/search-actions'; import {searchReducer as search} from '../../features/search/search-slice'; import { logStaticFilterDeselect, + toggleExcludeStaticFilterValue, toggleSelectStaticFilterValue, } from '../../features/static-filter-set/static-filter-set-actions'; import { @@ -50,6 +61,7 @@ import { DeselectableValue, FacetBreadcrumb, getBreadcrumbs, + GetBreadcrumbsConfiguration, NumericFacetBreadcrumb, StaticFilterBreadcrumb, } from '../core/breadcrumb-manager/headless-core-breadcrumb-manager'; @@ -84,10 +96,10 @@ export function buildBreadcrumbManager( const getState = () => engine.state; const getFacetBreadcrumbs = (): FacetBreadcrumb[] => { - return getBreadcrumbs( + const config: GetBreadcrumbsConfiguration> = { engine, - getState().facetSet, - ({facetId, selection}) => { + facetSet: getState().facetSet, + executeToggleSelect: ({facetId, selection}) => { const analyticsAction = logFacetBreadcrumb({ facetId: facetId, facetValue: selection.value, @@ -98,32 +110,59 @@ export function buildBreadcrumbManager( ); dispatch(executeSearch(analyticsAction)); }, - facetResponseActiveValuesSelector - ); + executeToggleExclude: ({facetId, selection}) => { + const analyticsAction = logFacetBreadcrumb({ + facetId: facetId, + facetValue: selection.value, + }); + dispatch(toggleExcludeFacetValue({facetId, selection})); + dispatch( + updateFreezeCurrentValues({facetId, freezeCurrentValues: false}) + ); + dispatch(executeSearch(analyticsAction)); + }, + facetValuesSelector: facetResponseActiveValuesSelector, + }; + + return getBreadcrumbs(config); }; const getNumericFacetBreadcrumbs = (): NumericFacetBreadcrumb[] => { - return getBreadcrumbs( + const config: GetBreadcrumbsConfiguration< + Record + > = { engine, - getState().numericFacetSet, - (payload) => { + facetSet: getState().numericFacetSet, + executeToggleSelect: (payload) => { dispatch(toggleSelectNumericFacetValue(payload)); dispatch(executeSearch(logNumericFacetBreadcrumb(payload))); }, - numericFacetSelectedValuesSelector - ); + executeToggleExclude: (payload) => { + dispatch(toggleExcludeNumericFacetValue(payload)); + dispatch(executeSearch(logNumericFacetBreadcrumb(payload))); + }, + facetValuesSelector: numericFacetActiveValuesSelector, + }; + return getBreadcrumbs(config); }; const getDateFacetBreadcrumbs = (): DateFacetBreadcrumb[] => { - return getBreadcrumbs( - engine, - getState().dateFacetSet, - (payload) => { - dispatch(toggleSelectDateFacetValue(payload)); - dispatch(executeSearch(logDateFacetBreadcrumb(payload))); - }, - dateFacetSelectedValuesSelector - ); + const config: GetBreadcrumbsConfiguration> = + { + engine, + facetSet: getState().dateFacetSet, + executeToggleSelect: (payload) => { + dispatch(toggleSelectDateFacetValue(payload)); + dispatch(executeSearch(logDateFacetBreadcrumb(payload))); + }, + executeToggleExclude: (payload) => { + dispatch(toggleExcludeDateFacetValue(payload)); + dispatch(executeSearch(logDateFacetBreadcrumb(payload))); + }, + facetValuesSelector: dateFacetActiveValuesSelector, + }; + + return getBreadcrumbs(config); }; const buildCategoryFacetBreadcrumb = (facetId: string) => { @@ -167,7 +206,7 @@ export function buildBreadcrumbManager( ): StaticFilterBreadcrumb => { const {id, values: filterValues} = filter; const values = filterValues - .filter((value) => value.state === 'selected') + .filter((value) => value.state !== 'idle') .map((value) => buildStaticFilterBreadcrumbValue(id, value)); return {id, values}; @@ -186,7 +225,11 @@ export function buildBreadcrumbManager( staticFilterValue: {caption, expression}, }); - dispatch(toggleSelectStaticFilterValue({id, value})); + if (value.state === 'selected') { + dispatch(toggleSelectStaticFilterValue({id, value})); + } else if (value.state === 'excluded') { + dispatch(toggleExcludeStaticFilterValue({id, value})); + } dispatch(executeSearch(analytics)); }, }; diff --git a/packages/headless/src/controllers/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts b/packages/headless/src/controllers/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts index fdd0f61d53a..9a69c209e6b 100644 --- a/packages/headless/src/controllers/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts +++ b/packages/headless/src/controllers/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts @@ -169,28 +169,17 @@ export interface DeselectableValue { deselect(): void; } -type InferFacetSliceValueRequestType = - T[string]['request']['currentValues'][number]; - -type InferFacetSliceValueType = - InferFacetSliceValueRequestType extends FacetValueRequest - ? FacetValue - : InferFacetSliceValueRequestType extends NumericRangeRequest - ? NumericFacetValue - : InferFacetSliceValueRequestType extends DateRangeRequest - ? DateFacetValue - : CategoryFacetValue; - /** * @internal * Get the breadcrumb of the facet selected * @param engine headless engine * @param facetSet facet section - * @param executeToggleSelect the execute toggle action + * @param executeToggleSelect execute the toggle select action + * @param executeToggleExclude execute the toggle exclude action * @param facetValuesSelector facet selector * @returns list breadcrumb of the facet selected */ -export const getBreadcrumbs = ( +export type GetBreadcrumbsConfiguration = { engine: CoreEngine< ConfigurationSection & SearchSection & @@ -198,29 +187,62 @@ export const getBreadcrumbs = ( NumericFacetSection & DateFacetSection & CategoryFacetSection - >, - facetSet: T, + >; + facetSet: T; executeToggleSelect: (payload: { facetId: string; selection: InferFacetSliceValueType; - }) => void, + }) => void; + executeToggleExclude: (payload: { + facetId: string; + selection: InferFacetSliceValueType; + }) => void; facetValuesSelector: ( - state: typeof engine['state'], + state: CoreEngine< + ConfigurationSection & + SearchSection & + FacetSection & + NumericFacetSection & + DateFacetSection & + CategoryFacetSection + >['state'], facetId: string - ) => InferFacetSliceValueType[] + ) => InferFacetSliceValueType[]; +}; + +type InferFacetSliceValueRequestType = + T[string]['request']['currentValues'][number]; + +export type InferFacetSliceValueType = + InferFacetSliceValueRequestType extends FacetValueRequest + ? FacetValue + : InferFacetSliceValueRequestType extends NumericRangeRequest + ? NumericFacetValue + : InferFacetSliceValueRequestType extends DateRangeRequest + ? DateFacetValue + : CategoryFacetValue; + +export const getBreadcrumbs = ( + config: GetBreadcrumbsConfiguration ): Breadcrumb>[] => { - return Object.keys(facetSet) + return Object.keys(config.facetSet) .map((facetId) => { - const values = facetValuesSelector(engine.state, facetId).map( - (selection) => ({ + const values = config + .facetValuesSelector(config.engine.state, facetId) + .map((selection) => ({ value: selection, - deselect: () => executeToggleSelect({facetId, selection}), - }) - ); + deselect: () => { + if (selection.state === 'selected') { + config.executeToggleSelect({facetId, selection}); + } else if (selection.state === 'excluded') { + config.executeToggleExclude({facetId, selection}); + } + }, + })); return { facetId, - field: facetSet[facetId]!.request.field, + field: config.facetSet[facetId]!.request.field, values, }; }) diff --git a/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.test.ts b/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.test.ts index c5053e0f1d5..6a5e94ad1ea 100644 --- a/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.test.ts +++ b/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.test.ts @@ -4,21 +4,28 @@ import {deselectAllCategoryFacetValues} from '../../../features/facets/category- import {categoryFacetSetReducer as categoryFacetSet} from '../../../features/facets/category-facet-set/category-facet-set-slice'; import {CategoryFacetValue} from '../../../features/facets/category-facet-set/interfaces/response'; import { + toggleExcludeFacetValue, toggleSelectFacetValue, updateFreezeCurrentValues, } from '../../../features/facets/facet-set/facet-set-actions'; import {facetSetReducer as facetSet} from '../../../features/facets/facet-set/facet-set-slice'; import {FacetValue} from '../../../features/facets/facet-set/interfaces/response'; -import {toggleSelectDateFacetValue} from '../../../features/facets/range-facets/date-facet-set/date-facet-actions'; +import { + toggleExcludeDateFacetValue, + toggleSelectDateFacetValue, +} from '../../../features/facets/range-facets/date-facet-set/date-facet-actions'; import {dateFacetSetReducer as dateFacetSet} from '../../../features/facets/range-facets/date-facet-set/date-facet-set-slice'; import {DateFacetValue} from '../../../features/facets/range-facets/date-facet-set/interfaces/response'; import {NumericFacetValue} from '../../../features/facets/range-facets/numeric-facet-set/interfaces/response'; -import {toggleSelectNumericFacetValue} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; +import { + toggleExcludeNumericFacetValue, + toggleSelectNumericFacetValue, +} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; import {numericFacetSetReducer as numericFacetSet} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-slice'; import {executeSearch} from '../../../features/search/search-actions'; import {searchReducer as search} from '../../../features/search/search-slice'; import {getSearchInitialState} from '../../../features/search/search-state'; -import {toggleSelectStaticFilterValue} from '../../../features/static-filter-set/static-filter-set-actions'; +import {toggleExcludeStaticFilterValue, toggleSelectStaticFilterValue} from '../../../features/static-filter-set/static-filter-set-actions'; import {InsightAppState} from '../../../state/insight-app-state'; import {buildMockCategoryFacetRequest} from '../../../test/mock-category-facet-request'; import {buildMockCategoryFacetResponse} from '../../../test/mock-category-facet-response'; @@ -80,20 +87,29 @@ describe('insight breadcrumb manager', () => { }); describe('facet breadcrumbs', () => { - let mockValue: FacetValue; + let mockSelectedValue: FacetValue; + let mockExcludedValue: FacetValue; let facetBreadcrumbs: FacetBreadcrumb[]; beforeEach(() => { - mockValue = buildMockFacetValue({ + mockSelectedValue = buildMockFacetValue({ state: 'selected', }); + mockExcludedValue = buildMockFacetValue({ + state: 'excluded', + }); state = buildMockInsightState({ search: { ...getSearchInitialState(), response: { ...getSearchInitialState().response, - facets: [buildMockFacetResponse({facetId, values: [mockValue]})], + facets: [ + buildMockFacetResponse({ + facetId, + values: [mockSelectedValue, mockExcludedValue], + }), + ], }, }, facetSet: { @@ -108,7 +124,8 @@ describe('insight breadcrumb manager', () => { }); it('#state gets facet breadcrumbs correctly', () => { - expect(facetBreadcrumbs[0].values[0].value).toBe(mockValue); + expect(facetBreadcrumbs[0].values[0].value).toBe(mockSelectedValue); + expect(facetBreadcrumbs[0].values[1].value).toBe(mockExcludedValue); }); it('dispatches an executeSearch action on selection', () => { @@ -121,7 +138,7 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, }) ); }); @@ -141,18 +158,55 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, + }) + ); + }); + + it('dispatches an executeSearch action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); + }); + + it('dispatches an toggleExcludeFacetValue action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.actions).toContainEqual( + toggleExcludeFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); + + it('dispatches an updateFreezeCurrentValues action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.actions).toContainEqual( + updateFreezeCurrentValues({ + facetId, + freezeCurrentValues: false, + }) + ); + }); + + it('dispatches a toggleExcludeFacetValue action when #deselectBreadcrumb is called', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeFacetValue({ + facetId, + selection: mockExcludedValue, }) ); }); }); describe('date facet breadcrumbs', () => { - let mockValue: DateFacetValue; + let mockSelectedValue: DateFacetValue; + let mockExcludedValue: DateFacetValue; let facetBreadcrumbs: DateFacetBreadcrumb[]; beforeEach(() => { - mockValue = buildMockDateFacetValue({state: 'selected'}); + mockSelectedValue = buildMockDateFacetValue({state: 'selected'}); + mockExcludedValue = buildMockDateFacetValue({state: 'excluded'}); state = buildMockInsightState({ search: { @@ -160,7 +214,10 @@ describe('insight breadcrumb manager', () => { response: { ...getSearchInitialState().response, facets: [ - buildMockDateFacetResponse({facetId, values: [mockValue]}), + buildMockDateFacetResponse({ + facetId, + values: [mockSelectedValue, mockExcludedValue], + }), ], }, }, @@ -176,7 +233,8 @@ describe('insight breadcrumb manager', () => { }); it('#state gets date facet breadcrumbs correctly', () => { - expect(facetBreadcrumbs[0].values[0].value).toBe(mockValue); + expect(facetBreadcrumbs[0].values[0].value).toBe(mockSelectedValue); + expect(facetBreadcrumbs[0].values[1].value).toBe(mockExcludedValue); }); it('dispatches an executeSearch action on selection', () => { @@ -189,7 +247,7 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectDateFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, }) ); }); @@ -199,18 +257,45 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectDateFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, + }) + ); + }); + + it('dispatches an executeSearch action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); + }); + + it('dispatches a toggleExcludeDateFacetValue action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.actions).toContainEqual( + toggleExcludeDateFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); + + it('dispatches a toggleExcludeDateFacetValue action when #deselectBreadcrumb is called', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeDateFacetValue({ + facetId, + selection: mockExcludedValue, }) ); }); }); describe('numeric facet breadcrumbs', () => { - let mockValue: NumericFacetValue; + let mockSelectedValue: NumericFacetValue; + let mockExcludedValue: NumericFacetValue; let facetBreadcrumbs: NumericFacetBreadcrumb[]; beforeEach(() => { - mockValue = buildMockNumericFacetValue({state: 'selected'}); + mockSelectedValue = buildMockNumericFacetValue({state: 'selected'}); + mockExcludedValue = buildMockNumericFacetValue({state: 'excluded'}); state = buildMockInsightState({ search: { @@ -218,7 +303,10 @@ describe('insight breadcrumb manager', () => { response: { ...getSearchInitialState().response, facets: [ - buildMockNumericFacetResponse({facetId, values: [mockValue]}), + buildMockNumericFacetResponse({ + facetId, + values: [mockSelectedValue, mockExcludedValue], + }), ], }, }, @@ -234,7 +322,8 @@ describe('insight breadcrumb manager', () => { }); it('#state gets numeric facet breadcrumbs correctly', () => { - expect(facetBreadcrumbs[0].values[0].value).toBe(mockValue); + expect(facetBreadcrumbs[0].values[0].value).toBe(mockSelectedValue); + expect(facetBreadcrumbs[0].values[1].value).toBe(mockExcludedValue); }); it('dispatches an executeSearch action on selection', () => { @@ -247,7 +336,7 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectNumericFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, }) ); }); @@ -257,19 +346,44 @@ describe('insight breadcrumb manager', () => { expect(engine.actions).toContainEqual( toggleSelectNumericFacetValue({ facetId, - selection: mockValue, + selection: mockSelectedValue, + }) + ); + }); + + it('dispatches an executeSearch action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.findAsyncAction(executeSearch.pending)).toBeTruthy(); + }); + + it('dispatches a toggleExcludeNumericFacetValue action on exclusion', () => { + facetBreadcrumbs[0].values[1].deselect(); + expect(engine.actions).toContainEqual( + toggleExcludeNumericFacetValue({ + facetId, + selection: mockExcludedValue, + }) + ); + }); + + it('dispatches a toggleExcludeNumericFacetValue action when #deselectBreadcrumb is called', () => { + breadcrumbManager.deselectBreadcrumb(facetBreadcrumbs[0].values[1]); + expect(engine.actions).toContainEqual( + toggleExcludeNumericFacetValue({ + facetId, + selection: mockExcludedValue, }) ); }); }); describe('category facet breadcrumbs', () => { - let mockValue: CategoryFacetValue; + let mockSelectedValue: CategoryFacetValue; let facetBreadcrumbs: CategoryFacetBreadcrumb[]; const otherFacetId = 'def456'; beforeEach(() => { - mockValue = buildMockCategoryFacetValue({state: 'selected'}); + mockSelectedValue = buildMockCategoryFacetValue({state: 'selected'}); state = buildMockInsightState({ search: { @@ -277,10 +391,13 @@ describe('insight breadcrumb manager', () => { response: { ...getSearchInitialState().response, facets: [ - buildMockCategoryFacetResponse({facetId, values: [mockValue]}), + buildMockCategoryFacetResponse({ + facetId, + values: [mockSelectedValue], + }), buildMockCategoryFacetResponse({ facetId: otherFacetId, - values: [mockValue], + values: [mockSelectedValue], }), ], }, @@ -304,8 +421,7 @@ describe('insight breadcrumb manager', () => { }); it('#state gets category facet breadcrumbs correctly', () => { - expect(facetBreadcrumbs[0].path).toEqual([mockValue]); - expect(facetBreadcrumbs[1].path).toEqual([mockValue]); + expect(facetBreadcrumbs[0].path).toEqual([mockSelectedValue]); }); it('dispatches an executeSearch action on selection', () => { @@ -335,10 +451,17 @@ describe('insight breadcrumb manager', () => { caption: 'c', state: 'selected', }); + const excluded = buildMockStaticFilterValue({ + caption: 'd', + state: 'excluded', + }); beforeEach(() => { state.staticFilterSet = { - [id]: buildMockStaticFilterSlice({id, values: [idle, selected]}), + [id]: buildMockStaticFilterSlice({ + id, + values: [idle, selected, excluded], + }), }; }); @@ -350,8 +473,9 @@ describe('insight breadcrumb manager', () => { expect(firstFilter.id).toBe(id); const {values} = firstFilter; - expect(values.length).toBe(1); + expect(values.length).toBe(2); expect(values[0].value.caption).toBe(selected.caption); + expect(values[1].value.caption).toBe(excluded.caption); }); it('#state.hasBreadcrumbs returns true', () => { @@ -359,24 +483,47 @@ describe('insight breadcrumb manager', () => { }); describe('#deselectBreadcrumb with a static filter breadcrumb value dispatches the correct actions', () => { - beforeEach(() => { - const {staticFilterBreadcrumbs} = breadcrumbManager.state; - const [firstBreadcrumb] = staticFilterBreadcrumbs[0].values; + describe('#selected values', () => { + beforeEach(() => { + const {staticFilterBreadcrumbs} = breadcrumbManager.state; + const [firstBreadcrumb] = staticFilterBreadcrumbs[0].values; - breadcrumbManager.deselectBreadcrumb(firstBreadcrumb); - }); + breadcrumbManager.deselectBreadcrumb(firstBreadcrumb); + }); - it('dispatches #toggleSelectStaticFilterValue', () => { - const toggleSelect = toggleSelectStaticFilterValue({ - id, - value: selected, + it('dispatches #toggleSelectStaticFilterValue', () => { + const toggleSelect = toggleSelectStaticFilterValue({ + id, + value: selected, + }); + expect(engine.actions).toContainEqual(toggleSelect); + }); + + it('dispatches #executeSearch', () => { + const action = engine.findAsyncAction(executeSearch.pending); + expect(action).toBeTruthy(); }); - expect(engine.actions).toContainEqual(toggleSelect); }); + describe('#excluded values', () => { + beforeEach(() => { + const {staticFilterBreadcrumbs} = breadcrumbManager.state; + const [, excludedBreadcrumb] = staticFilterBreadcrumbs[0].values; + + breadcrumbManager.deselectBreadcrumb(excludedBreadcrumb); + }); + + it('dispatches #toggleExcludeStaticFilterValue', () => { + const toggleExclude = toggleExcludeStaticFilterValue({ + id, + value: excluded, + }); + expect(engine.actions).toContainEqual(toggleExclude); + }); - it('dispatches #executeSearch', () => { - const action = engine.findAsyncAction(executeSearch.pending); - expect(action).toBeTruthy(); + it('dispatches #executeSearch', () => { + const action = engine.findAsyncAction(executeSearch.pending); + expect(action).toBeTruthy(); + }); }); }); }); @@ -385,14 +532,31 @@ describe('insight breadcrumb manager', () => { state.numericFacetSet[facetId] = buildMockNumericFacetSlice({ request: buildMockNumericFacetRequest({facetId}), }); - const mockValue = buildMockNumericFacetValue({state: 'selected'}); + const mockSelectedValue = buildMockNumericFacetValue({state: 'selected'}); + state.search.response.facets = [ + buildMockNumericFacetResponse({ + facetId, + values: [mockSelectedValue], + }), + ]; + expect(breadcrumbManager.state.hasBreadcrumbs).toBe(true); + }); + + it('hasBreadcrumbs returns true when a facet value is excluded', () => { + state.numericFacetSet[facetId] = buildMockNumericFacetSlice({ + request: buildMockNumericFacetRequest({facetId}), + }); + const mockExcludedValue = buildMockNumericFacetValue({state: 'excluded'}); state.search.response.facets = [ - buildMockNumericFacetResponse({facetId, values: [mockValue]}), + buildMockNumericFacetResponse({ + facetId, + values: [mockExcludedValue], + }), ]; expect(breadcrumbManager.state.hasBreadcrumbs).toBe(true); }); - it('hasBreadcrumbs returns false when no facet value is selected', () => { + it('hasBreadcrumbs returns false when no facet value is selected or excluded', () => { state.search.response.facets = []; expect(breadcrumbManager.state.hasBreadcrumbs).toBe(false); }); diff --git a/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.ts b/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.ts index 6eec15f9aa3..007f4f95f63 100644 --- a/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.ts +++ b/packages/headless/src/controllers/insight/breadcrumb-manager/headless-insight-breadcrumb-manager.ts @@ -5,24 +5,37 @@ import {logCategoryFacetBreadcrumb} from '../../../features/facets/category-face import {categoryFacetResponseSelectedValuesSelector} from '../../../features/facets/category-facet-set/category-facet-set-selectors'; import {categoryFacetSetReducer as categoryFacetSet} from '../../../features/facets/category-facet-set/category-facet-set-slice'; import { + toggleExcludeFacetValue, toggleSelectFacetValue, updateFreezeCurrentValues, } from '../../../features/facets/facet-set/facet-set-actions'; import {logFacetBreadcrumb} from '../../../features/facets/facet-set/facet-set-insight-analytics-actions'; -import {facetResponseSelectedValuesSelector} from '../../../features/facets/facet-set/facet-set-selectors'; +import {facetResponseActiveValuesSelector} from '../../../features/facets/facet-set/facet-set-selectors'; import {facetSetReducer as facetSet} from '../../../features/facets/facet-set/facet-set-slice'; +import {FacetSlice} from '../../../features/facets/facet-set/facet-set-state'; import {logClearBreadcrumbs} from '../../../features/facets/generic/facet-generic-insight-analytics-actions'; -import {toggleSelectDateFacetValue} from '../../../features/facets/range-facets/date-facet-set/date-facet-actions'; +import { + toggleExcludeDateFacetValue, + toggleSelectDateFacetValue, +} from '../../../features/facets/range-facets/date-facet-set/date-facet-actions'; import {logDateFacetBreadcrumb} from '../../../features/facets/range-facets/date-facet-set/date-facet-insight-analytics-actions'; -import {dateFacetSelectedValuesSelector} from '../../../features/facets/range-facets/date-facet-set/date-facet-selectors'; +import {dateFacetActiveValuesSelector} from '../../../features/facets/range-facets/date-facet-set/date-facet-selectors'; import {dateFacetSetReducer as dateFacetSet} from '../../../features/facets/range-facets/date-facet-set/date-facet-set-slice'; -import {toggleSelectNumericFacetValue} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; +import {DateFacetSlice} from '../../../features/facets/range-facets/date-facet-set/date-facet-set-state'; +import { + toggleExcludeNumericFacetValue, + toggleSelectNumericFacetValue, +} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-actions'; import {logNumericFacetBreadcrumb} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-insight-analytics-actions'; -import {numericFacetSelectedValuesSelector} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-selectors'; +import {numericFacetActiveValuesSelector} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-selectors'; import {numericFacetSetReducer as numericFacetSet} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-slice'; +import {NumericFacetSlice} from '../../../features/facets/range-facets/numeric-facet-set/numeric-facet-set-state'; import {executeSearch} from '../../../features/insight-search/insight-search-actions'; import {searchReducer as search} from '../../../features/search/search-slice'; -import {toggleSelectStaticFilterValue} from '../../../features/static-filter-set/static-filter-set-actions'; +import { + toggleExcludeStaticFilterValue, + toggleSelectStaticFilterValue, +} from '../../../features/static-filter-set/static-filter-set-actions'; import {logInsightStaticFilterDeselect} from '../../../features/static-filter-set/static-filter-set-insight-analytics-actions'; import { StaticFilterSlice, @@ -48,6 +61,7 @@ import { DeselectableValue, FacetBreadcrumb, getBreadcrumbs, + GetBreadcrumbsConfiguration, NumericFacetBreadcrumb, StaticFilterBreadcrumb, } from '../../core/breadcrumb-manager/headless-core-breadcrumb-manager'; @@ -81,10 +95,10 @@ export function buildBreadcrumbManager( const getState = () => engine.state; const getFacetBreadcrumbs = (): FacetBreadcrumb[] => { - return getBreadcrumbs( + const config: GetBreadcrumbsConfiguration> = { engine, - getState().facetSet, - ({facetId, selection}) => { + facetSet: getState().facetSet, + executeToggleSelect: ({facetId, selection}) => { const analyticsAction = logFacetBreadcrumb({ facetId: facetId, facetValue: selection.value, @@ -95,32 +109,59 @@ export function buildBreadcrumbManager( ); dispatch(executeSearch(analyticsAction)); }, - facetResponseSelectedValuesSelector - ); + executeToggleExclude: ({facetId, selection}) => { + const analyticsAction = logFacetBreadcrumb({ + facetId: facetId, + facetValue: selection.value, + }); + dispatch(toggleExcludeFacetValue({facetId, selection})); + dispatch( + updateFreezeCurrentValues({facetId, freezeCurrentValues: false}) + ); + dispatch(executeSearch(analyticsAction)); + }, + facetValuesSelector: facetResponseActiveValuesSelector, + }; + return getBreadcrumbs(config); }; const getNumericFacetBreadcrumbs = (): NumericFacetBreadcrumb[] => { - return getBreadcrumbs( + const config: GetBreadcrumbsConfiguration< + Record + > = { engine, - getState().numericFacetSet, - (payload) => { + facetSet: getState().numericFacetSet, + executeToggleSelect: (payload) => { dispatch(toggleSelectNumericFacetValue(payload)); dispatch(executeSearch(logNumericFacetBreadcrumb(payload))); }, - numericFacetSelectedValuesSelector - ); + executeToggleExclude: (payload) => { + dispatch(toggleExcludeNumericFacetValue(payload)); + dispatch(executeSearch(logNumericFacetBreadcrumb(payload))); + }, + facetValuesSelector: numericFacetActiveValuesSelector, + }; + + return getBreadcrumbs(config); }; const getDateFacetBreadcrumbs = (): DateFacetBreadcrumb[] => { - return getBreadcrumbs( - engine, - getState().dateFacetSet, - (payload) => { - dispatch(toggleSelectDateFacetValue(payload)); - dispatch(executeSearch(logDateFacetBreadcrumb(payload))); - }, - dateFacetSelectedValuesSelector - ); + const config: GetBreadcrumbsConfiguration> = + { + engine, + facetSet: getState().dateFacetSet, + executeToggleSelect: (payload) => { + dispatch(toggleSelectDateFacetValue(payload)); + dispatch(executeSearch(logDateFacetBreadcrumb(payload))); + }, + executeToggleExclude: (payload) => { + dispatch(toggleExcludeDateFacetValue(payload)); + dispatch(executeSearch(logDateFacetBreadcrumb(payload))); + }, + facetValuesSelector: dateFacetActiveValuesSelector, + }; + + return getBreadcrumbs(config); }; const buildCategoryFacetBreadcrumb = (facetId: string) => { @@ -164,7 +205,7 @@ export function buildBreadcrumbManager( ): StaticFilterBreadcrumb => { const {id, values: filterValues} = filter; const values = filterValues - .filter((value) => value.state === 'selected') + .filter((value) => value.state !== 'idle') .map((value) => buildStaticFilterBreadcrumbValue(id, value)); return {id, values}; @@ -183,7 +224,11 @@ export function buildBreadcrumbManager( staticFilterValue: {caption, expression}, }); - dispatch(toggleSelectStaticFilterValue({id, value})); + if (value.state === 'selected') { + dispatch(toggleSelectStaticFilterValue({id, value})); + } else if (value.state === 'excluded') { + dispatch(toggleExcludeStaticFilterValue({id, value})); + } dispatch(executeSearch(analytics)); }, }; diff --git a/packages/headless/src/features/facets/range-facets/date-facet-set/date-facet-selectors.ts b/packages/headless/src/features/facets/range-facets/date-facet-set/date-facet-selectors.ts index f66e34ac938..d1826f474d2 100644 --- a/packages/headless/src/features/facets/range-facets/date-facet-set/date-facet-selectors.ts +++ b/packages/headless/src/features/facets/range-facets/date-facet-set/date-facet-selectors.ts @@ -35,6 +35,16 @@ export const dateFacetSelectedValuesSelector = ( return facetResponse.values.filter((value) => value.state === 'selected'); }; +export const dateFacetActiveValuesSelector = ( + state: SearchSection & DateFacetSection, + facetId: string +): DateFacetValue[] => { + const facetResponse = dateFacetResponseSelector(state, facetId) || { + values: [], + }; + return facetResponse.values.filter((value) => value.state !== 'idle'); +}; + export const dateFacetExcludedValuesSelector = ( state: SearchSection & DateFacetSection, facetId: string diff --git a/packages/headless/src/features/facets/range-facets/numeric-facet-set/numeric-facet-selectors.ts b/packages/headless/src/features/facets/range-facets/numeric-facet-set/numeric-facet-selectors.ts index 8f689e691ba..ad828ff0323 100644 --- a/packages/headless/src/features/facets/range-facets/numeric-facet-set/numeric-facet-selectors.ts +++ b/packages/headless/src/features/facets/range-facets/numeric-facet-set/numeric-facet-selectors.ts @@ -25,6 +25,16 @@ export const numericFacetResponseSelector = ( return undefined; }; +export const numericFacetActiveValuesSelector = ( + state: SearchSection & NumericFacetSection, + facetId: string +): NumericFacetValue[] => { + const facetResponse = numericFacetResponseSelector(state, facetId) || { + values: [], + }; + return facetResponse.values.filter((value) => value.state !== 'idle'); +}; + export const numericFacetSelectedValuesSelector = ( state: SearchSection & NumericFacetSection, facetId: string