Skip to content

Commit

Permalink
feat(headless): add url manager support for automatic facets (#3033)
Browse files Browse the repository at this point in the history
  • Loading branch information
aprudhomme-coveo authored Jul 24, 2023
1 parent 6f936ea commit 0e85aca
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 58 deletions.
2 changes: 1 addition & 1 deletion packages/headless/src/api/search/search/search-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -57,33 +59,33 @@ 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'
);
});

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};
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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({
Expand Down Expand Up @@ -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';

Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export function getCoreActiveSearchParameters(
...getCategoryFacets(state),
...getNumericFacets(state),
...getDateFacets(state),
...getAutomaticFacets(state),
};
}

Expand Down Expand Up @@ -259,3 +260,17 @@ function getDateFacets(state: Partial<SearchParametersState>) {
function getSelectedRanges<T extends RangeValueRequest>(ranges: T[]) {
return ranges.filter((range) => range.state === 'selected');
}

function getAutomaticFacets(state: Partial<SearchParametersState>) {
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} : {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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,
setDesiredCount,
toggleSelectAutomaticFacetValue,
} from './automatic-facet-set-actions';
import {getAutomaticFacetSetInitialState} from './automatic-facet-set-state';
import {AutomaticFacetResponse} from './interfaces/response';

export const automaticFacetSetReducer = createReducer(
getAutomaticFacetSetInitialState(),
Expand Down Expand Up @@ -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,
};
}
Loading

0 comments on commit 0e85aca

Please sign in to comment.