Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(headless): support facet exclusion in search parameter manager #3122

Merged
merged 10 commits into from
Aug 24, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ describe('search parameter manager', () => {
});
});

describe('#state.parameters.fExcluded', () => {
it('only includes excluded values when a facet has some', () => {
const excluded = buildMockFacetValueRequest({
value: 'a',
state: 'excluded',
});
const idle = buildMockFacetValueRequest({value: 'b', state: 'idle'});
const selected = buildMockFacetValueRequest({
value: 'c',
state: 'selected',
});

const currentValues = [excluded, idle, selected];
engine.state.facetSet = {
author: buildMockFacetSlice({
request: buildMockFacetRequest({currentValues}),
}),
};

expect(manager.state.parameters.fExcluded).toEqual({author: ['a']});
});

it('is not included when there are no facets with selected values', () => {
engine.state.facetSet = {author: buildMockFacetSlice()};
expect(manager.state.parameters).not.toContain('fExcluded');
});
});

describe('#state.parameters.cf', () => {
it('only includes selected values when a category facet has some', () => {
const selected = buildMockCategoryFacetValueRequest({
Expand Down Expand Up @@ -265,7 +293,10 @@ describe('search parameter manager', () => {
});

it('is possible to access every relevant search parameter using #state.parameters given a certain initial state', () => {
const facetValues = [buildMockFacetValueRequest({state: 'selected'})];
const facetValues = [
buildMockFacetValueRequest({state: 'selected'}),
buildMockFacetValueRequest({state: 'excluded'}),
];
engine.state.facetSet = {
author: buildMockFacetSlice({
request: buildMockFacetRequest({currentValues: facetValues}),
Expand Down Expand Up @@ -309,7 +340,6 @@ describe('search parameter manager', () => {
});
engine.state.automaticFacetSet.set = {a: slice};


engine.state.query.q = 'a';
engine.state.sortCriteria = 'qre';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export function getCoreActiveSearchParameters(
...getQ(state),
...getTab(state),
...getSortCriteria(state),
...getFacets(state),
...getFacets(state, getSelectedValues, 'f'),
...getFacets(state, getExcludedValues, 'fExcluded'),
...getCategoryFacets(state),
...getNumericFacets(state),
...getDateFacets(state),
Expand Down Expand Up @@ -185,26 +186,34 @@ function getSortCriteria(state: Partial<SearchParametersState>) {
return shouldInclude ? {sortCriteria} : {};
}

function getFacets(state: Partial<SearchParametersState>) {
function getFacets(
state: Partial<SearchParametersState>,
valuesSelector: (currentValues: FacetValueRequest[]) => string[],
out: keyof SearchParameters
) {
if (state.facetSet === undefined) {
return {};
}

const f = Object.entries(state.facetSet)
const facets = Object.entries(state.facetSet)
.filter(([facetId]) => state.facetOptions?.facets[facetId]?.enabled ?? true)
.map(([facetId, {request}]) => {
const selectedValues = getSelectedValues(request.currentValues);
return selectedValues.length ? {[facetId]: selectedValues} : {};
const facetValues = valuesSelector(request.currentValues);
return facetValues.length ? {[facetId]: facetValues} : {};
})
.reduce((acc, obj) => ({...acc, ...obj}), {});

return Object.keys(f).length ? {f} : {};
return Object.keys(facets).length ? {[out]: facets} : {};
}

function getSelectedValues(values: FacetValueRequest[]) {
return values.filter((fv) => fv.state === 'selected').map((fv) => fv.value);
}

function getExcludedValues(values: FacetValueRequest[]) {
return values.filter((fv) => fv.state === 'excluded').map((fv) => fv.value);
}

function getCategoryFacets(state: Partial<SearchParametersState>) {
if (state.categoryFacetSet === undefined) {
return {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ describe('search parameter manager', () => {

it(`given a certain initial state,
it is possible to access every relevant search parameter using #state.parameters`, () => {
const facetValues = [buildMockFacetValueRequest({state: 'selected'})];
const facetValues = [
buildMockFacetValueRequest({state: 'selected'}),
buildMockFacetValueRequest({state: 'excluded'}),
];
engine.state.facetSet = {
author: buildMockFacetSlice({
request: buildMockFacetRequest({currentValues: facetValues}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export interface SearchParameters {
*/
f?: Record<string, string[]>;

/**
* A record of the excluded facets, where the key is the facet id, and value is an array containing the excluded values.
*/
fExcluded?: Record<string, string[]>;

/**
* A zero-based index of the first result.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
logFacetClearAll,
logFacetDeselect,
logFacetSelect,
logFacetExclude,
} from '../facets/facet-set/facet-set-analytics-actions';
import {
logPageNumber,
Expand Down Expand Up @@ -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(
Expand All @@ -67,41 +70,77 @@ 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']}}),
logFacetSelect({facetId: 'author', facetValue: 'Cervantes'})
);
});

it(`should log #logFacetDeselect when an ${parameter} parameter with a single value is removed`, () => {
it(`should log #logFacetSelect when an ${parameter} parameter is modified & a value added`, () => {
expectIdenticalActionType(
logParametersChange({[parameter]: {author: ['Cervantes']}}, {}),
logFacetDeselect({facetId: 'author', facetValue: 'Cervantes'})
logParametersChange(
{[parameter]: {author: ['Cervantes']}},
{[parameter]: {author: ['Cervantes', 'Orwell']}}
),
logFacetSelect({facetId: 'author', facetValue: 'Orwell'})
);
});
}

it(`should log #logFacetClearAll when an ${parameter} parameter with multiple values is removed`, () => {
function testFacetExcludeLogging(
expectIdenticalActionType: (
action1: SearchAction,
action2: SearchAction
) => void
) {
testFacetLogging('fExcluded', expectIdenticalActionType);

it('should log #logFacetSelect when an fExcluded parameter is added', () => {
expectIdenticalActionType(
logParametersChange({[parameter]: {author: ['Cervantes', 'Orwell']}}, {}),
logFacetClearAll('author')
logParametersChange({}, {fExcluded: {author: ['Cervantes']}}),
logFacetExclude({facetId: 'author', facetValue: 'Cervantes'})
);
});

it(`should log #logFacetSelect when an ${parameter} parameter is modified & a value added`, () => {
it('should log #logFacetSelect when an fExcluded parameter is modified & a value added', () => {
expectIdenticalActionType(
logParametersChange(
{[parameter]: {author: ['Cervantes']}},
{[parameter]: {author: ['Cervantes', 'Orwell']}}
{fExcluded: {author: ['Cervantes']}},
{fExcluded: {author: ['Cervantes', 'Orwell']}}
),
logFacetSelect({facetId: 'author', facetValue: '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']}}, {}),
logFacetDeselect({facetId: 'author', facetValue: 'Cervantes'})
);
});

it(`should log #logFacetClearAll when an ${parameter} parameter with multiple values is removed`, () => {
expectIdenticalActionType(
logParametersChange({[parameter]: {author: ['Cervantes', 'Orwell']}}, {}),
logFacetClearAll('author')
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
logFacetClearAll,
logFacetDeselect,
logFacetSelect,
logFacetExclude,
} from '../../features/facets/facet-set/facet-set-analytics-actions';
import {logSearchboxSubmit} from '../../features/query/query-analytics-actions';
import {SearchParameters} from '../../features/search-parameters/search-parameter-actions';
Expand Down Expand Up @@ -38,6 +39,16 @@ export function logParametersChange(
return logFacetAnalyticsAction(previousParameters.f, newParameters.f);
}

if (
areFacetParamsEqual(previousParameters.fExcluded, newParameters.fExcluded)
) {
return logFacetAnalyticsAction(
previousParameters.fExcluded,
newParameters.fExcluded,
true
);
}

if (areFacetParamsEqual(previousParameters.cf, newParameters.cf)) {
return logFacetAnalyticsAction(previousParameters.cf, newParameters.cf);
}
Expand Down Expand Up @@ -92,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);
Expand All @@ -108,10 +120,15 @@ function logFacetAnalyticsAction(
const addedIds = newIds.filter((id) => !previousIds.includes(id));
if (addedIds.length) {
const facetId = addedIds[0];
return logFacetSelect({
facetId,
facetValue: newFacets[facetId][0],
});
return excluded
? logFacetExclude({
facetId,
facetValue: newFacets[facetId][0],
})
: logFacetSelect({
facetId,
facetValue: newFacets[facetId][0],
});
}

const facetIdWithDifferentValues = newIds.find((key) =>
Expand All @@ -131,10 +148,15 @@ function logFacetAnalyticsAction(
);

if (addedValues.length) {
return logFacetSelect({
facetId: facetIdWithDifferentValues,
facetValue: addedValues[0],
});
return excluded
? logFacetExclude({
facetId: facetIdWithDifferentValues,
facetValue: addedValues[0],
})
: logFacetSelect({
facetId: facetIdWithDifferentValues,
facetValue: addedValues[0],
});
}

const removedValues = previousValues.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const searchParametersDefinition: SchemaDefinition<
numberOfResults: new NumberValue({min: 0}),
sortCriteria: new StringValue(),
f: new RecordValue(),
fExcluded: new RecordValue(),
cf: new RecordValue(),
nf: new RecordValue(),
df: new RecordValue(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function initialSearchParameterSelector(
getPaginationInitialState().defaultNumberOfResults,
sortCriteria: getSortCriteriaInitialState(),
f: {},
fExcluded: {},
cf: {},
nf: {},
df: {},
Expand Down
Loading
Loading