diff --git a/packages/headless/src/features/facet-options/facet-options-slice.ts b/packages/headless/src/features/facet-options/facet-options-slice.ts index 26b44549d24..17d5700bcf5 100644 --- a/packages/headless/src/features/facet-options/facet-options-slice.ts +++ b/packages/headless/src/features/facet-options/facet-options-slice.ts @@ -58,6 +58,7 @@ export const facetOptionsReducer = createReducer( .addCase(restoreSearchParameters, (state, action) => { [ ...Object.keys(action.payload.f ?? {}), + ...Object.keys(action.payload.fExcluded ?? {}), ...Object.keys(action.payload.cf ?? {}), ...Object.keys(action.payload.nf ?? {}), ...Object.keys(action.payload.df ?? {}), diff --git a/packages/headless/src/features/facets/facet-set/facet-set-slice.ts b/packages/headless/src/features/facets/facet-set/facet-set-slice.ts index 61f8bbc9714..a724cc00caa 100644 --- a/packages/headless/src/features/facets/facet-set/facet-set-slice.ts +++ b/packages/headless/src/features/facets/facet-set/facet-set-slice.ts @@ -64,22 +64,27 @@ export const facetSetReducer = createReducer( }) .addCase(restoreSearchParameters, (state, action) => { const f = action.payload.f || {}; + const fExcluded = action.payload.fExcluded || {}; const facetIds = Object.keys(state); facetIds.forEach((id) => { const {request} = state[id]!; const selectedValues = f[id] || []; + const excludedValues = fExcluded[id] || []; const idleValues = request.currentValues.filter( - (facetValue) => !selectedValues.includes(facetValue.value) + (facetValue) => + !selectedValues.includes(facetValue.value) && + !excludedValues.includes(facetValue.value) ); request.currentValues = [ ...selectedValues.map(buildSelectedFacetValueRequest), + ...excludedValues.map(buildExcludedFacetValueRequest), ...idleValues.map(restoreFacetValueToIdleState), ]; request.preventAutoSelect = selectedValues.length > 0; request.numberOfValues = Math.max( - selectedValues.length, + selectedValues.length + excludedValues.length, request.numberOfValues ); }); @@ -299,6 +304,10 @@ function buildSelectedFacetValueRequest(value: string): FacetValueRequest { return {value, state: 'selected'}; } +function buildExcludedFacetValueRequest(value: string): FacetValueRequest { + return {value, state: 'excluded'}; +} + function restoreFacetValueToIdleState( facetValue: FacetValueRequest ): FacetValueRequest { diff --git a/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.test.ts b/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.test.ts index 41012ec29cb..49443e3c692 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.test.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.test.ts @@ -5,6 +5,7 @@ import { logFacetClearAll, logFacetDeselect, logFacetSelect, + logFacetExclude, } from '../facets/facet-set/facet-set-analytics-actions'; import { logPageNumber, @@ -53,11 +54,13 @@ describe('logParametersChange', () => { ); }); - testFacetLogging('f', expectIdenticalActionType); + testFacetSelectLogging('f', expectIdenticalActionType); - testFacetLogging('af', expectIdenticalActionType); + testFacetSelectLogging('af', expectIdenticalActionType); - testFacetLogging('cf', expectIdenticalActionType); + testFacetSelectLogging('cf', expectIdenticalActionType); + + testFacetExcludeLogging(expectIdenticalActionType); it('should log a generic #logInterfaceChange when an unmanaged parameter', () => { expectIdenticalActionType( @@ -67,13 +70,15 @@ describe('logParametersChange', () => { }); }); -function testFacetLogging( +function testFacetSelectLogging( parameter: string, expectIdenticalActionType: ( action1: SearchAction, action2: SearchAction ) => void ) { + testFacetLogging(parameter, expectIdenticalActionType); + it(`should log #logFacetSelect when an ${parameter} parameter is added`, () => { expectIdenticalActionType( logParametersChange({}, {[parameter]: {author: ['Cervantes']}}), @@ -81,6 +86,50 @@ function testFacetLogging( ); }); + it(`should log #logFacetSelect when an ${parameter} parameter is modified & a value added`, () => { + expectIdenticalActionType( + logParametersChange( + {[parameter]: {author: ['Cervantes']}}, + {[parameter]: {author: ['Cervantes', 'Orwell']}} + ), + logFacetSelect({facetId: 'author', facetValue: 'Orwell'}) + ); + }); +} + +function testFacetExcludeLogging( + expectIdenticalActionType: ( + action1: SearchAction, + action2: SearchAction + ) => void +) { + /*testFacetLogging('fExcluded', expectIdenticalActionType); + + it('should log #logFacetSelect when an fExcluded parameter is added', () => { + expectIdenticalActionType( + logParametersChange({}, {fExcluded: {author: ['Cervantes']}}), + logFacetExclude({facetId: 'author', facetValue: 'Cervantes'}) + ); + });*/ + + it('should log #logFacetSelect when an fExcluded parameter is modified & a value added', () => { + expectIdenticalActionType( + logParametersChange( + {fExcluded: {author: ['Cervantes']}}, + {fExcluded: {author: ['Cervantes', 'Orwell']}} + ), + logFacetExclude({facetId: 'author', facetValue: 'Orwell'}) + ); + }); +} + +function testFacetLogging( + parameter: string, + expectIdenticalActionType: ( + action1: SearchAction, + action2: SearchAction + ) => void +) { it(`should log #logFacetDeselect when an ${parameter} parameter with a single value is removed`, () => { expectIdenticalActionType( logParametersChange({[parameter]: {author: ['Cervantes']}}, {}), @@ -95,16 +144,6 @@ function testFacetLogging( ); }); - it(`should log #logFacetSelect when an ${parameter} parameter is modified & a value added`, () => { - expectIdenticalActionType( - logParametersChange( - {[parameter]: {author: ['Cervantes']}}, - {[parameter]: {author: ['Cervantes', 'Orwell']}} - ), - logFacetSelect({facetId: 'author', facetValue: 'Orwell'}) - ); - }); - it(`should log #logFacetDeselect when an ${parameter} parameter is modified & a value removed`, () => { expectIdenticalActionType( logParametersChange( diff --git a/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.ts b/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.ts index 03cf561f1e8..d70824df77b 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-analytics-actions.ts @@ -44,7 +44,8 @@ export function logParametersChange( ) { return logFacetAnalyticsAction( previousParameters.fExcluded, - newParameters.fExcluded + newParameters.fExcluded, + true ); } @@ -102,7 +103,8 @@ function parseRangeFacetParams(facetsParams: RangeFacetParameters) { function logFacetAnalyticsAction( previousFacets: FacetParameters = {}, - newFacets: FacetParameters = {} + newFacets: FacetParameters = {}, + excluded = false ): SearchAction { const previousIds = Object.keys(previousFacets); const newIds = Object.keys(newFacets); @@ -118,15 +120,14 @@ function logFacetAnalyticsAction( const addedIds = newIds.filter((id) => !previousIds.includes(id)); if (addedIds.length) { const facetId = addedIds[0]; - const facetValue = newFacets[facetId][0]; - return facetValue === 'selected' - ? logFacetSelect({ + return excluded + ? logFacetExclude({ facetId, - facetValue: facetValue, + facetValue: newFacets[facetId][0], }) - : logFacetExclude({ + : logFacetSelect({ facetId, - facetValue: facetValue, + facetValue: newFacets[facetId][0], }); } @@ -147,12 +148,12 @@ function logFacetAnalyticsAction( ); if (addedValues.length) { - return addedValues[0] === 'selected' - ? logFacetSelect({ + return excluded + ? logFacetExclude({ facetId: facetIdWithDifferentValues, facetValue: addedValues[0], }) - : logFacetExclude({ + : logFacetSelect({ facetId: facetIdWithDifferentValues, facetValue: addedValues[0], }); diff --git a/packages/headless/src/features/search-parameters/search-parameter-serializer.test.ts b/packages/headless/src/features/search-parameters/search-parameter-serializer.test.ts index c4c6c384187..1a4489c96e3 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-serializer.test.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-serializer.test.ts @@ -51,6 +51,12 @@ describe('buildSearchParameterSerializer', () => { expect(result).toEqual('f-author=a,b&f-filetype=c,d'); }); + it('serializes the #fExcluded parameter correctly', () => { + const fExcluded = {author: ['a', 'b'], filetype: ['c', 'd']}; + const result = serialize({fExcluded}); + expect(result).toEqual('fExcluded-author=a,b&fExcluded-filetype=c,d'); + }); + it('serializes special characters in the #f parameter correctly', () => { someSpecialCharactersThatNeedsEncoding.forEach((specialChar) => { const f = {author: ['a', specialChar]}; diff --git a/packages/headless/src/features/search-parameters/search-parameter-serializer.ts b/packages/headless/src/features/search-parameters/search-parameter-serializer.ts index 64a5654edb1..de7a76f78d8 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-serializer.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-serializer.ts @@ -39,7 +39,13 @@ function serializePair(pair: [string, unknown]) { return ''; } - if (key === 'f' || key === 'cf' || key === 'sf' || key === 'af') { + if ( + key === 'f' || + key === 'fExcluded' || + key === 'cf' || + key === 'sf' || + key === 'af' + ) { return isFacetObject(val) ? serializeFacets(key, val) : ''; } @@ -303,8 +309,8 @@ function castUnknownObject(value: string) { function keyHasObjectValue( key: SearchParameterKey -): key is 'f' | 'cf' | 'nf' | 'df' | 'sf' | 'af' { - const keys = ['f', 'cf', 'nf', 'df', 'sf', 'af']; +): key is 'f' | 'fExcluded' | 'cf' | 'nf' | 'df' | 'sf' | 'af' { + const keys = ['f', 'fExcluded', 'cf', 'nf', 'df', 'sf', 'af']; return keys.includes(key); }