From 0e85aca2123d0acf0b0b03f374008f1e0f6792a0 Mon Sep 17 00:00:00 2001 From: aprudhomme-coveo <132079077+aprudhomme-coveo@users.noreply.github.com> Date: Mon, 24 Jul 2023 11:31:41 -0400 Subject: [PATCH] feat(headless): add url manager support for automatic facets (#3033) https://coveord.atlassian.net/browse/KIT-2584 --- .../src/api/search/search/search-response.ts | 2 +- ...less-core-search-parameter-manager.test.ts | 90 ++++++++++++------- .../headless-core-search-parameter-manager.ts | 15 ++++ .../headless-automatic-facet-builder.ts | 4 +- .../headless-search-parameter-manager.test.ts | 7 ++ .../automatic-facet-set-slice.test.ts | 15 ++++ .../automatic-facet-set-slice.ts | 36 ++++++++ .../search-parameter-actions.ts | 5 ++ ...search-parameter-analytics-actions.test.ts | 54 ++++++----- .../search-parameter-analytics-actions.ts | 4 + .../search-parameter-schema.ts | 1 + .../search-parameter-selectors.ts | 1 + .../search-parameter-serializer.test.ts | 25 +++++- .../search-parameter-serializer.ts | 9 +- .../src/test/mock-search-parameters.ts | 1 + 15 files changed, 211 insertions(+), 58 deletions(-) diff --git a/packages/headless/src/api/search/search/search-response.ts b/packages/headless/src/api/search/search/search-response.ts index 7a0a765058e..b2541f2d683 100644 --- a/packages/headless/src/api/search/search/search-response.ts +++ b/packages/headless/src/api/search/search/search-response.ts @@ -15,7 +15,7 @@ import {SecurityIdentity} from './security-identity'; import {PhrasesToHighlight, TermsToHighlight} from './stemming'; export interface SearchResponseSuccess { - generateAutomaticFacets: AutomaticFacets; + generateAutomaticFacets?: AutomaticFacets; termsToHighlight: TermsToHighlight; phrasesToHighlight: PhrasesToHighlight; results: Result[]; diff --git a/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.test.ts b/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.test.ts index 999e8c66f46..43ce15093df 100644 --- a/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.test.ts +++ b/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.test.ts @@ -1,6 +1,7 @@ import {restoreSearchParameters} from '../../../features/search-parameters/search-parameter-actions'; import {initialSearchParameterSelector} from '../../../features/search-parameters/search-parameter-selectors'; import {buildMockSearchAppEngine, MockSearchEngine} from '../../../test'; +import {buildMockAutomaticFacetResponse} from '../../../test/mock-automatic-facet-response'; import {buildMockCategoryFacetRequest} from '../../../test/mock-category-facet-request'; import {buildMockCategoryFacetSlice} from '../../../test/mock-category-facet-slice'; import {buildMockCategoryFacetValueRequest} from '../../../test/mock-category-facet-value-request'; @@ -9,6 +10,7 @@ import {buildMockDateFacetSlice} from '../../../test/mock-date-facet-slice'; import {buildMockDateFacetValue} from '../../../test/mock-date-facet-value'; import {buildMockFacetRequest} from '../../../test/mock-facet-request'; import {buildMockFacetSlice} from '../../../test/mock-facet-slice'; +import {buildMockFacetValue} from '../../../test/mock-facet-value'; import {buildMockFacetValueRequest} from '../../../test/mock-facet-value-request'; import {buildMockNumericFacetRequest} from '../../../test/mock-numeric-facet-request'; import {buildMockNumericFacetSlice} from '../../../test/mock-numeric-facet-slice'; @@ -57,7 +59,7 @@ describe('search parameter manager', () => { expect(engine.actions).toContainEqual(action); }); - it('when #parameters is not an object, it throws an error', () => { + it('throws an error when #parameters is not an object', () => { props.initialState.parameters = true as never; expect(() => initSearchParameterManager()).toThrow( 'Check the initialState of buildSearchParameterManager' @@ -65,25 +67,25 @@ describe('search parameter manager', () => { }); describe('#state.parameters.q', () => { - it('when the parameter does not equal the default value, it is included', () => { + it('is included when the parameter does not equal the default value', () => { engine.state.query.q = 'a'; expect(manager.state.parameters.q).toBe('a'); }); - it('when the parameter is equal to the default value, it is not included', () => { - expect('q' in manager.state.parameters).toBe(false); + it('is not included when the parameter is equal to the default value', () => { + expect(manager.state.parameters).not.toContain('q'); }); }); describe('#state.parameters.tab', () => { - it('when there is an active tab, it is included', () => { + it('is included when there is an active tab', () => { const id = 'a'; const tab = buildMockTabSlice({id, isActive: true}); engine.state.tabSet = {[id]: tab}; expect(manager.state.parameters.tab).toBe(id); }); - it('when there is no active tab, it is not included', () => { + it('is not included when there is no active tab', () => { const id = 'a'; const tab = buildMockTabSlice({id, isActive: false}); engine.state.tabSet = {[id]: tab}; @@ -92,7 +94,7 @@ describe('search parameter manager', () => { }); describe('#state.parameters.f', () => { - it('when a facet has selected values, only selected values are included', () => { + it('only includes selected values when a facet has some', () => { const selected = buildMockFacetValueRequest({ value: 'a', state: 'selected', @@ -109,14 +111,14 @@ describe('search parameter manager', () => { expect(manager.state.parameters.f).toEqual({author: ['a']}); }); - it('when there are no facets with selected values, the #f parameter is not included', () => { + it('is not included when there are no facets with selected values', () => { engine.state.facetSet = {author: buildMockFacetSlice()}; - expect('f' in manager.state.parameters).toBe(false); + expect(manager.state.parameters).not.toContain('f'); }); }); describe('#state.parameters.cf', () => { - it('when a category facet has selected values, only selected values are included', () => { + it('only includes selected values when a category facet has some', () => { const selected = buildMockCategoryFacetValueRequest({ value: 'a', state: 'selected', @@ -138,7 +140,7 @@ describe('search parameter manager', () => { expect(manager.state.parameters.cf).toEqual({author: ['a']}); }); - it('when a category facet has a nested selection, the full path is included', () => { + it('includes the full path when a category facet has a nested selection', () => { const child = buildMockCategoryFacetValueRequest({ value: 'b', state: 'selected', @@ -157,14 +159,14 @@ describe('search parameter manager', () => { expect(manager.state.parameters.cf).toEqual({author: ['a', 'b']}); }); - it('when there are no category facets with selected values, the #cf parameter is not included', () => { + it('is not included when there are no category facets with selected values', () => { engine.state.categoryFacetSet = {author: buildMockCategoryFacetSlice()}; - expect('cf' in manager.state.parameters).toBe(false); + expect(manager.state.parameters).not.toContain('cf'); }); }); describe('#state.parameters.nf', () => { - it('when a numeric facet has selected values, only selected values are included', () => { + it('only includes selected values when a numeric facet has some', () => { const selected = buildMockNumericFacetValue({ start: 0, end: 10, @@ -186,14 +188,14 @@ describe('search parameter manager', () => { expect(manager.state.parameters.nf).toEqual({size: [selected]}); }); - it('when there are no numeric facets with selected values, the #nf parameter is not included', () => { + it('is not included when there are no numeric facets with selected values', () => { engine.state.numericFacetSet = {author: buildMockNumericFacetSlice()}; - expect('nf' in manager.state.parameters).toBe(false); + expect(manager.state.parameters).not.toContain('nf'); }); }); describe('#state.parameters.df', () => { - it('when a date facet has selected values, only selected values are included', () => { + it('only includes selected values when a date facet has some', () => { const selected = buildMockDateFacetValue({ start: '2020/10/01', end: '2020/11/01', @@ -215,30 +217,53 @@ describe('search parameter manager', () => { expect(manager.state.parameters.df).toEqual({created: [selected]}); }); - it('when there are no date facets with selected values, the #df parameter is not included', () => { + it('is not included when there are no date facets with selected values', () => { engine.state.dateFacetSet = {created: buildMockDateFacetSlice()}; - expect('df' in manager.state.parameters).toBe(false); + expect(manager.state.parameters).not.toContain('df'); + }); + }); + + describe('#state.parameters.af', () => { + it('only includes selected values when a facet has some', () => { + const selected = buildMockFacetValue({ + value: 'a', + state: 'selected', + }); + const idle = buildMockFacetValue({value: 'b', state: 'idle'}); + + const currentValues = [selected, idle]; + engine.state.automaticFacetSet.facets = { + author: buildMockAutomaticFacetResponse({values: currentValues}), + }; + + expect(manager.state.parameters.af).toEqual({author: ['a']}); + }); + + it('is not included when there are no facets with selected values', () => { + engine.state.automaticFacetSet.facets = { + author: buildMockAutomaticFacetResponse(), + }; + expect(manager.state.parameters).not.toContain('af'); }); }); describe('#state.parameters.sortCriteria', () => { - it('when the parameter does not equal the default value, it is included', () => { + it('is included when the parameter does not equal the default value, ', () => { engine.state.sortCriteria = 'qre'; expect(manager.state.parameters.sortCriteria).toBe('qre'); }); - it('when the parameter is equal to the default value, it is not included', () => { - expect('sortCriteria' in manager.state.parameters).toBe(false); + it('is not included when the parameter is equal to the default value', () => { + expect(manager.state.parameters).not.toContain('sortCriteria'); }); - it('when the parameter is undefined, it is not included', () => { + it('is not included when the parameter is undefined', () => { engine.state.sortCriteria = undefined as never; - expect('sortCriteria' in manager.state.parameters).toBe(false); + expect(manager.state.parameters).not.toContain('sortCriteria'); }); }); - it(`given a certain initial state, - it is possible to access every relevant search parameter using #state.parameters`, () => { + it('is possible to access every relevant search parameter using #state.parameters given a certain initial state', () => { const facetValues = [buildMockFacetValueRequest({state: 'selected'})]; engine.state.facetSet = { author: buildMockFacetSlice({ @@ -277,6 +302,11 @@ describe('search parameter manager', () => { const tab = buildMockTabSlice({id: 'a', isActive: true}); engine.state.tabSet = {a: tab}; + const automaticFacetValues = [buildMockFacetValue({state: 'selected'})]; + engine.state.automaticFacetSet.facets = { + a: buildMockAutomaticFacetResponse({values: automaticFacetValues}), + }; + engine.state.query.q = 'a'; engine.state.sortCriteria = 'qre'; @@ -296,7 +326,7 @@ describe('search parameter manager', () => { }); describe('#synchronize', () => { - it('given partial search parameters, it dispatches #restoreSearchParameters with non-specified parameters set to their initial values', () => { + it('it dispatches #restoreSearchParameters with non-specified parameters set to their initial values given partial search parameters', () => { const params = {q: 'a'}; manager.synchronize(params); @@ -311,7 +341,7 @@ describe('search parameter manager', () => { }); describe('#validateParams', () => { - it('with initial params, should return true', () => { + it('should return true with initial params', () => { const initialParameters = initialSearchParameterSelector(engine.state); expect(validateParams(engine, initialParameters)).toBe(true); @@ -325,7 +355,7 @@ describe('search parameter manager', () => { }; }); - it('with an existing tab parameter, should return true', () => { + it('should return true with an existing tab parameter', () => { const initialParameters = initialSearchParameterSelector(engine.state); expect( @@ -336,7 +366,7 @@ describe('search parameter manager', () => { ).toBe(true); }); - it('with a non-existing tab parameter, should return false', () => { + it('should return false with a non-existing tab parameter', () => { const initialParameters = initialSearchParameterSelector(engine.state); expect( diff --git a/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.ts b/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.ts index 1a6b9e8e5d9..558fc7c2de6 100644 --- a/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.ts +++ b/packages/headless/src/controllers/core/search-parameter-manager/headless-core-search-parameter-manager.ts @@ -133,6 +133,7 @@ export function getCoreActiveSearchParameters( ...getCategoryFacets(state), ...getNumericFacets(state), ...getDateFacets(state), + ...getAutomaticFacets(state), }; } @@ -259,3 +260,17 @@ function getDateFacets(state: Partial) { function getSelectedRanges(ranges: T[]) { return ranges.filter((range) => range.state === 'selected'); } + +function getAutomaticFacets(state: Partial) { + if (state.automaticFacetSet?.facets === undefined) { + return {}; + } + const af = Object.entries(state.automaticFacetSet.facets) + .map(([facetId, {values}]) => { + const selectedValues = getSelectedValues(values); + return selectedValues.length ? {[facetId]: selectedValues} : {}; + }) + .reduce((acc, obj) => ({...acc, ...obj}), {}); + + return Object.keys(af).length ? {af} : {}; +} diff --git a/packages/headless/src/controllers/facets/automatic-facet-builder/headless-automatic-facet-builder.ts b/packages/headless/src/controllers/facets/automatic-facet-builder/headless-automatic-facet-builder.ts index 2cef2683647..f0e31e8935e 100644 --- a/packages/headless/src/controllers/facets/automatic-facet-builder/headless-automatic-facet-builder.ts +++ b/packages/headless/src/controllers/facets/automatic-facet-builder/headless-automatic-facet-builder.ts @@ -77,9 +77,9 @@ export function buildAutomaticFacetBuilder( get state() { const automaticFacets = - engine.state.search.response.generateAutomaticFacets.facets.map( + engine.state.search.response.generateAutomaticFacets?.facets.map( (facet) => buildAutomaticFacet(engine, {field: facet.field}) - ); + ) ?? []; return { automaticFacets, }; diff --git a/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.test.ts b/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.test.ts index b961da5de8c..77ee377228c 100644 --- a/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.test.ts +++ b/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.test.ts @@ -2,6 +2,7 @@ import {restoreSearchParameters} from '../../features/search-parameters/search-p import {initialSearchParameterSelector} from '../../features/search-parameters/search-parameter-selectors'; import {executeSearch} from '../../features/search/search-actions'; import {buildMockSearchAppEngine, MockSearchEngine} from '../../test'; +import {buildMockAutomaticFacetResponse} from '../../test/mock-automatic-facet-response'; import {buildMockCategoryFacetRequest} from '../../test/mock-category-facet-request'; import {buildMockCategoryFacetSlice} from '../../test/mock-category-facet-slice'; import {buildMockCategoryFacetValueRequest} from '../../test/mock-category-facet-value-request'; @@ -10,6 +11,7 @@ import {buildMockDateFacetSlice} from '../../test/mock-date-facet-slice'; import {buildMockDateFacetValue} from '../../test/mock-date-facet-value'; import {buildMockFacetRequest} from '../../test/mock-facet-request'; import {buildMockFacetSlice} from '../../test/mock-facet-slice'; +import {buildMockFacetValue} from '../../test/mock-facet-value'; import {buildMockFacetValueRequest} from '../../test/mock-facet-value-request'; import {buildMockNumericFacetRequest} from '../../test/mock-numeric-facet-request'; import {buildMockNumericFacetSlice} from '../../test/mock-numeric-facet-slice'; @@ -202,6 +204,11 @@ describe('search parameter manager', () => { const tab = buildMockTabSlice({id: 'a', isActive: true}); engine.state.tabSet = {a: tab}; + const automaticFacetValues = [buildMockFacetValue({state: 'selected'})]; + engine.state.automaticFacetSet.facets = { + a: buildMockAutomaticFacetResponse({values: automaticFacetValues}), + }; + engine.state.query.q = 'a'; engine.state.query.enableQuerySyntax = true; engine.state.advancedSearchQueries.aq = 'someAq'; diff --git a/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.test.ts b/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.test.ts index 5ff76e22dbc..a32b9fc6d89 100644 --- a/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.test.ts +++ b/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.test.ts @@ -2,6 +2,7 @@ import {buildMockAutomaticFacetResponse} from '../../../test/mock-automatic-face import {buildMockFacetValue} from '../../../test/mock-facet-value'; import {buildMockSearch} from '../../../test/mock-search'; import {logSearchEvent} from '../../analytics/analytics-actions'; +import {restoreSearchParameters} from '../../search-parameters/search-parameter-actions'; import {executeSearch} from '../../search/search-actions'; import {FacetValueState} from '../facet-api/value'; import { @@ -163,4 +164,18 @@ describe('automatic-facet-set slice', () => { ); }); }); + + describe('#restoreSearchParameters', () => { + it('it sets #values to the selected values in the payload when a facet is found in the #af payload', () => { + const field = 'field'; + const value = 'a'; + const af = {[field]: [value]}; + const action = restoreSearchParameters({af}); + + const finalState = automaticFacetSetReducer(state, action); + const selectedValue = buildMockFacetValue({value, state: 'selected'}); + + expect(finalState.facets[field].values).toEqual([selectedValue]); + }); + }); }); diff --git a/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.ts b/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.ts index 7c855421aab..735db0c70d3 100644 --- a/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.ts +++ b/packages/headless/src/features/facets/automatic-facet-set/automatic-facet-set-slice.ts @@ -1,4 +1,6 @@ import {createReducer} from '@reduxjs/toolkit'; +import {FacetValue} from '../../../product-listing.index'; +import {restoreSearchParameters} from '../../search-parameters/search-parameter-actions'; import {executeSearch} from '../../search/search-actions'; import { deselectAllAutomaticFacetValues, @@ -6,6 +8,7 @@ import { toggleSelectAutomaticFacetValue, } from './automatic-facet-set-actions'; import {getAutomaticFacetSetInitialState} from './automatic-facet-set-state'; +import {AutomaticFacetResponse} from './interfaces/response'; export const automaticFacetSetReducer = createReducer( getAutomaticFacetSetInitialState(), @@ -48,6 +51,39 @@ export const automaticFacetSetReducer = createReducer( for (const value of facet.values) { value.state = 'idle'; } + }) + .addCase(restoreSearchParameters, (state, action) => { + const af = action.payload.af ?? {}; + + for (const field in af) { + const response = buildTemporaryAutomaticFacetResponse(field); + const values = af[field].map((value) => + buildTemporarySelectedFacetValue(value) + ); + response.values.push(...values); + + state.facets[field] = response; + } }); } ); + +function buildTemporaryAutomaticFacetResponse( + field: string +): AutomaticFacetResponse { + return { + field, + values: [], + moreValuesAvailable: false, + label: '', + indexScore: 0, + }; +} + +function buildTemporarySelectedFacetValue(value: string): FacetValue { + return { + value, + state: 'selected', + numberOfResults: 0, + }; +} diff --git a/packages/headless/src/features/search-parameters/search-parameter-actions.ts b/packages/headless/src/features/search-parameters/search-parameter-actions.ts index 2dea876369c..c5c56863f57 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-actions.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-actions.ts @@ -77,6 +77,11 @@ export interface SearchParameters { * The active tab id. */ tab?: string; + + /** + * A record of the automatic facets, where the key is the facet id, and value is an array containing the selected values. + */ + af?: Record; } export const restoreSearchParameters = createAction( 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 02ef84b4078..41012ec29cb 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 @@ -53,51 +53,65 @@ describe('logParametersChange', () => { ); }); - it('should log #logFacetSelect when an #f parameter is added', () => { + testFacetLogging('f', expectIdenticalActionType); + + testFacetLogging('af', expectIdenticalActionType); + + testFacetLogging('cf', expectIdenticalActionType); + + it('should log a generic #logInterfaceChange when an unmanaged parameter', () => { expectIdenticalActionType( - logParametersChange({}, {f: {author: ['Cervantes']}}), + logParametersChange({}, {cq: 'hello'}), + logInterfaceChange() + ); + }); +}); + +function testFacetLogging( + parameter: string, + expectIdenticalActionType: ( + action1: SearchAction, + action2: SearchAction + ) => void +) { + it(`should log #logFacetSelect when an ${parameter} parameter is added`, () => { + expectIdenticalActionType( + logParametersChange({}, {[parameter]: {author: ['Cervantes']}}), logFacetSelect({facetId: 'author', facetValue: 'Cervantes'}) ); }); - it('should log #logFacetDeselect when an #f parameter with a single value is removed', () => { + it(`should log #logFacetDeselect when an ${parameter} parameter with a single value is removed`, () => { expectIdenticalActionType( - logParametersChange({f: {author: ['Cervantes']}}, {}), + logParametersChange({[parameter]: {author: ['Cervantes']}}, {}), logFacetDeselect({facetId: 'author', facetValue: 'Cervantes'}) ); }); - it('should log #logFacetClearAll when an #f parameter with a multiple values is removed', () => { + it(`should log #logFacetClearAll when an ${parameter} parameter with multiple values is removed`, () => { expectIdenticalActionType( - logParametersChange({f: {author: ['Cervantes', 'Orwell']}}, {}), + logParametersChange({[parameter]: {author: ['Cervantes', 'Orwell']}}, {}), logFacetClearAll('author') ); }); - it('should log #logFacetSelect when an #f parameter is modified & a value added', () => { + it(`should log #logFacetSelect when an ${parameter} parameter is modified & a value added`, () => { expectIdenticalActionType( logParametersChange( - {f: {author: ['Cervantes']}}, - {f: {author: ['Cervantes', 'Orwell']}} + {[parameter]: {author: ['Cervantes']}}, + {[parameter]: {author: ['Cervantes', 'Orwell']}} ), logFacetSelect({facetId: 'author', facetValue: 'Orwell'}) ); }); - it('should log #logFacetDeselect when an #f parameter is modified & a value removed', () => { + it(`should log #logFacetDeselect when an ${parameter} parameter is modified & a value removed`, () => { expectIdenticalActionType( logParametersChange( - {f: {author: ['Cervantes', 'Orwell']}}, - {f: {author: ['Cervantes']}} + {[parameter]: {author: ['Cervantes', 'Orwell']}}, + {[parameter]: {author: ['Cervantes']}} ), logFacetDeselect({facetId: 'author', facetValue: 'Orwell'}) ); }); - - it('should log a generic #logInterfaceChange when an unmanaged parameter', () => { - expectIdenticalActionType( - logParametersChange({}, {cq: 'hello'}), - logInterfaceChange() - ); - }); -}); +} 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 aa28803fbdb..e88f2dbd80b 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 @@ -42,6 +42,10 @@ export function logParametersChange( return logFacetAnalyticsAction(previousParameters.cf, newParameters.cf); } + if (areFacetParamsEqual(previousParameters.af, newParameters.af)) { + return logFacetAnalyticsAction(previousParameters.af, newParameters.af); + } + if (areFacetParamsEqual(previousParameters.nf, newParameters.nf)) { return logRangeFacetAnalyticsAction( previousParameters.nf, diff --git a/packages/headless/src/features/search-parameters/search-parameter-schema.ts b/packages/headless/src/features/search-parameters/search-parameter-schema.ts index e75c5f7dda6..151b5fcc84e 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-schema.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-schema.ts @@ -24,4 +24,5 @@ export const searchParametersDefinition: SchemaDefinition< debug: new BooleanValue(), sf: new RecordValue(), tab: new StringValue(), + af: new RecordValue(), }; diff --git a/packages/headless/src/features/search-parameters/search-parameter-selectors.ts b/packages/headless/src/features/search-parameters/search-parameter-selectors.ts index d1ba43d3c64..3d347d21f75 100644 --- a/packages/headless/src/features/search-parameters/search-parameter-selectors.ts +++ b/packages/headless/src/features/search-parameters/search-parameter-selectors.ts @@ -30,5 +30,6 @@ export function initialSearchParameterSelector( debug: getDebugInitialState(), sf: {}, tab: '', + af: {}, }; } 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 8efa756f786..c4c6c384187 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 @@ -197,7 +197,29 @@ describe('buildSearchParameterSerializer', () => { }, }); }); + it('deserializes two automatic facets correctly', () => { + const result = deserialize('af-author=a,b&af-filetype=c,d'); + expect(result).toEqual({ + af: { + author: ['a', 'b'], + filetype: ['c', 'd'], + }, + }); + }); + it('deserializes two automatic facets correctly with special characters', () => { + someSpecialCharactersThatNeedsEncoding.forEach((char) => { + const result = deserialize( + `af-author=${encodeURIComponent(char)},b&af-filetype=c,d` + ); + expect(result).toEqual({ + af: { + author: [char, 'b'], + filetype: ['c', 'd'], + }, + }); + }); + }); it('deserializes two facets correctly', () => { const result = deserialize('f-author=a,b&f-filetype=c,d'); expect(result).toEqual({ @@ -464,7 +486,8 @@ describe('buildSearchParameterSerializer', () => { ], }; const sf = {fileType: ['a', 'b']}; - const parameters = buildMockSearchParameters({f, cf, nf, df, sf}); + const af = {documenttype: ['s', 'sd']}; + const parameters = buildMockSearchParameters({f, cf, nf, df, sf, af}); const {serialize, deserialize} = buildSearchParameterSerializer(); const serialized = serialize(parameters); 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 774f32d3c6f..8fb9a115ef6 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,7 @@ function serializePair(pair: [string, unknown]) { return ''; } - if (key === 'f' || key === 'cf' || key === 'sf') { + if (key === 'f' || key === 'cf' || key === 'sf' || key === 'af') { return isFacetObject(val) ? serializeFacets(key, val) : ''; } @@ -148,7 +148,7 @@ function splitOnFirstEqual(str: string) { function preprocessObjectPairs(pair: string[]) { const [key, val] = pair; - const objectKey = /^(f|cf|nf|df|sf)-(.+)$/; + const objectKey = /^(f|cf|nf|df|sf|af)-(.+)$/; const result = objectKey.exec(key); if (!result) { @@ -257,6 +257,7 @@ function isValidKey(key: string): key is SearchParameterKey { debug: true, sf: true, tab: true, + af: true, }; return key in supportedParameters; @@ -301,8 +302,8 @@ function castUnknownObject(value: string) { function keyHasObjectValue( key: SearchParameterKey -): key is 'f' | 'cf' | 'nf' | 'df' | 'sf' { - const keys = ['f', 'cf', 'nf', 'df', 'sf']; +): key is 'f' | 'cf' | 'nf' | 'df' | 'sf' | 'af' { + const keys = ['f', 'cf', 'nf', 'df', 'sf', 'af']; return keys.includes(key); } diff --git a/packages/headless/src/test/mock-search-parameters.ts b/packages/headless/src/test/mock-search-parameters.ts index d0ae745ecdd..d75a553efa4 100644 --- a/packages/headless/src/test/mock-search-parameters.ts +++ b/packages/headless/src/test/mock-search-parameters.ts @@ -18,6 +18,7 @@ export function buildMockSearchParameters( debug: false, sf: {}, tab: '', + af: {}, ...config, }; }