Skip to content

Commit

Permalink
feat(commerce): add location facets (#4562)
Browse files Browse the repository at this point in the history
Add support for location facets. We went with the separate controller
approach as we believe the location facets might evolve differently from
the regular facets over time.

In follow-up PRs I:
- Added support for breadcrumbs and parameters:
#4571
- Added support in the context controller to pass-in the latitude and
longitude: #4572
- Exported the actions through an actions loader:
#4569
  • Loading branch information
Spuffynism authored Oct 23, 2024
1 parent cde560e commit 1dbd9a9
Show file tree
Hide file tree
Showing 20 changed files with 745 additions and 21 deletions.
7 changes: 7 additions & 0 deletions packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from './features/commerce/sort/sort-actions-loader.js';
export * from './features/commerce/facets/core-facet/core-facet-actions-loader.js';
export * from './features/commerce/facets/category-facet/category-facet-actions-loader.js';
export * from './features/commerce/facets/regular-facet/regular-facet-actions-loader.js';
// TODO COMHUB-247 export location facets actions loader
export * from './features/commerce/facets/date-facet/date-facet-actions-loader.js';
export * from './features/commerce/facets/numeric-facet/numeric-facet-actions-loader.js';
export * from './features/commerce/query-set/query-set-actions-loader.js';
Expand Down Expand Up @@ -164,6 +165,10 @@ export type {
RegularFacet,
RegularFacetState,
} from './controllers/commerce/core/facets/regular/headless-commerce-regular-facet.js';
export type {
LocationFacet,
LocationFacetState,
} from './controllers/commerce/core/facets/location/headless-commerce-location-facet.js';
export type {
NumericFacet,
NumericFacetState,
Expand All @@ -178,6 +183,8 @@ export type {
FacetType,
FacetValueRequest,
RegularFacetValue,
LocationFacetValueRequest,
LocationFacetValue,
NumericRangeRequest,
NumericFacetValue,
DateRangeRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
BaseFacetValue,
CategoryFacetResponse,
DateFacetResponse,
LocationFacetResponse,
NumericFacetResponse,
RegularFacetResponse,
} from '../../../../features/commerce/facets/facet-set/interfaces/response.js';
Expand Down Expand Up @@ -116,7 +117,8 @@ interface ActionCreators {

const facetTypeWithoutExcludeAction: FacetType = 'hierarchical';

const actions: Record<FacetType, ActionCreators> = {
// TODO: COMHUB-247 add support for location facet
const actions: Record<Exclude<FacetType, 'location'>, ActionCreators> = {
regular: {
toggleSelectActionCreator: toggleSelectFacetValue,
toggleExcludeActionCreator: toggleExcludeFacetValue,
Expand Down Expand Up @@ -153,7 +155,10 @@ export function buildCoreBreadcrumbManager(
const controller = buildController(engine);
const {dispatch} = engine;

const createBreadcrumb = (facet: AnyFacetResponse) => ({
// TODO: COMHUB-247 add support for location facet
const createBreadcrumb = (
facet: Exclude<AnyFacetResponse, LocationFacetResponse>
) => ({
facetId: facet.facetId,
facetDisplayName: facet.displayName,
field: facet.field,
Expand Down Expand Up @@ -253,7 +258,9 @@ export function buildCoreBreadcrumbManager(
(facetOrder): BreadcrumbManagerState => {
const breadcrumbs = facetOrder.flatMap((facetId) => {
const facet = options.facetResponseSelector(engine[stateKey], facetId);
if (hasActiveValue(facet)) {

// TODO: COMHUB-247 add support for location facet
if (hasActiveValue(facet) && facet.type !== 'location') {
return [createBreadcrumb(facet)];
}
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
buildMockCommerceDateFacetResponse,
buildMockCommerceNumericFacetResponse,
buildMockCommerceRegularFacetResponse,
buildMockCommerceLocationFacetResponse,
} from '../../../../../test/mock-commerce-facet-response.js';
import {buildMockCommerceState} from '../../../../../test/mock-commerce-state.js';
import {
Expand Down Expand Up @@ -55,6 +56,9 @@ describe('SSR FacetGenerator', () => {
case 'numericalRange':
response = buildMockCommerceNumericFacetResponse({facetId, type});
break;
case 'location':
response = buildMockCommerceLocationFacetResponse({facetId, type});
break;
case 'regular':
default:
response = buildMockCommerceRegularFacetResponse({facetId, type});
Expand Down Expand Up @@ -117,6 +121,10 @@ describe('SSR FacetGenerator', () => {
facetId: 'regular-facet',
type: 'regular',
},
{
facetId: 'location-facet',
type: 'location',
},
];
state = buildMockCommerceState();
setFacetState(facetsInEngineState);
Expand All @@ -131,7 +139,7 @@ describe('SSR FacetGenerator', () => {
expect(facetGenerator).toBeTruthy();
});
it('#state is an array containing the state of each facet', () => {
expect(facetGenerator.state.length).toBe(4);
expect(facetGenerator.state.length).toBe(5);
expect(
facetGenerator.state.map((facet) => ({
facetId: facet.facetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {stateKey} from '../../../../../app/state-key.js';
import {facetRequestSelector} from '../../../../../features/commerce/facets/facet-set/facet-set-selector.js';
import {
AnyFacetResponse,
LocationFacetValue,
RegularFacetValue,
} from '../../../../../features/commerce/facets/facet-set/interfaces/response.js';
import {manualNumericFacetSelector} from '../../../../../features/commerce/facets/numeric-facet/manual-numeric-facet-selectors.js';
Expand Down Expand Up @@ -44,6 +45,11 @@ import {
FacetType,
getCoreFacetState,
} from '../headless-core-commerce-facet.js';
import {
getLocationFacetState,
LocationFacet,
LocationFacetState,
} from '../location/headless-commerce-location-facet.js';
import {
getNumericFacetState,
NumericFacet,
Expand Down Expand Up @@ -72,6 +78,9 @@ export type {
RegularFacet,
RegularFacetState,
RegularFacetValue,
LocationFacet,
LocationFacetState,
LocationFacetValue,
};

export type FacetGeneratorState = MappedFacetStates;
Expand All @@ -87,7 +96,9 @@ type MappedFacetState = {
? DateFacetState
: T extends 'hierarchical'
? CategoryFacetState
: never;
: T extends 'location'
? LocationFacetState
: never;
};

export function defineFacetGenerator<
Expand Down Expand Up @@ -235,6 +246,10 @@ export function buildFacetGenerator(
createFacetState(facetResponseSelector) as RegularFacetState,
specificFacetSearchStateSelector(getEngineState(), facetId)
);
case 'location':
return getLocationFacetState(
createFacetState(facetResponseSelector) as LocationFacetState
);
}
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('CSR FacetGenerator', () => {
let facetGenerator: FacetGenerator;
const mockBuildNumericFacet = vi.fn();
const mockBuildRegularFacet = vi.fn();
const mockBuildLocationFacet = vi.fn();
const mockBuildDateFacet = vi.fn();
const mockBuildCategoryFacet = vi.fn();
const mockFetchProductsActionCreator = vi.fn();
Expand Down Expand Up @@ -61,6 +62,7 @@ describe('CSR FacetGenerator', () => {
options = {
buildNumericFacet: mockBuildNumericFacet,
buildRegularFacet: mockBuildRegularFacet,
buildLocationFacet: mockBuildLocationFacet,
buildDateFacet: mockBuildDateFacet,
buildCategoryFacet: mockBuildCategoryFacet,
fetchProductsActionCreator: mockFetchProductsActionCreator,
Expand Down Expand Up @@ -97,6 +99,14 @@ describe('CSR FacetGenerator', () => {
expect(mockBuildRegularFacet).toHaveBeenCalledWith(engine, {facetId});
});

it('when engine facet state contains a location facet, generates a location facet controller', () => {
const facetId = 'location_facet_id';
setFacetState([{facetId, type: 'location'}]);

expect(facetGenerator.facets.length).toEqual(1);
expect(mockBuildLocationFacet).toHaveBeenCalledWith(engine, {facetId});
});

it('when engine facet state contains a numeric facet, generates a numeric facet controller', () => {
const facetId = 'numeric_facet_id';
setFacetState([{facetId, type: 'numericalRange'}]);
Expand Down Expand Up @@ -127,6 +137,10 @@ describe('CSR FacetGenerator', () => {
facetId: 'regular_facet_id',
type: 'regular',
},
{
facetId: 'location_facet_id',
type: 'location',
},
{
facetId: 'numeric_facet_id',
type: 'numericalRange',
Expand All @@ -142,24 +156,29 @@ describe('CSR FacetGenerator', () => {
];
setFacetState(facets);

let index = 0;
mockBuildRegularFacet.mockReturnValue({
state: {facetId: facets[0].facetId},
state: {facetId: facets[index++].facetId},
});
mockBuildLocationFacet.mockReturnValue({
state: {facetId: facets[index++].facetId},
});
mockBuildNumericFacet.mockReturnValue({
state: {facetId: facets[1].facetId},
state: {facetId: facets[index++].facetId},
});
mockBuildDateFacet.mockReturnValue({
state: {facetId: facets[index++].facetId},
});
mockBuildDateFacet.mockReturnValue({state: {facetId: facets[2].facetId}});
mockBuildCategoryFacet.mockReturnValue({
state: {facetId: facets[3].facetId},
state: {facetId: facets[index++].facetId},
});

const facetState = facetGenerator.facets;

expect(facetState.length).toEqual(4);
expect(facetState[0].state.facetId).toEqual(facets[0].facetId);
expect(facetState[1].state.facetId).toEqual(facets[1].facetId);
expect(facetState[2].state.facetId).toEqual(facets[2].facetId);
expect(facetState[3].state.facetId).toEqual(facets[3].facetId);
expect(facetState.length).toEqual(5);
expect(facetState.map((f) => f.state.facetId)).toEqual(
facets.map((f) => f.facetId)
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
CommerceFacetOptions,
CoreCommerceFacet,
} from '../headless-core-commerce-facet.js';
import {LocationFacet} from '../location/headless-commerce-location-facet.js';
import {NumericFacet} from '../numeric/headless-commerce-numeric-facet.js';
import {RegularFacet} from '../regular/headless-commerce-regular-facet.js';
import {SearchableFacetOptions} from '../searchable/headless-commerce-searchable-facet.js';
Expand All @@ -47,7 +48,7 @@ export interface FacetGenerator extends Controller {

/**
* The facet sub-controllers created by the facet generator.
* Array of [RegularFacet](./regular-facet), [DateRangeFacet](./date-range-facet), [NumericFacet](./numeric-facet), and [CategoryFacet](./category-facet).
* Array of [RegularFacet](./regular-facet), [DateRangeFacet](./date-range-facet), [NumericFacet](./numeric-facet), [CategoryFacet](./category-facet), and [LocationFacet](./location-facet).
*/
facets: GeneratedFacetControllers;

Expand Down Expand Up @@ -79,7 +80,9 @@ export type MappedGeneratedFacetController = {
? DateFacet
: T extends 'hierarchical'
? CategoryFacet
: never;
: T extends 'location'
? LocationFacet
: never;
};

type CommerceFacetBuilder<
Expand Down Expand Up @@ -108,6 +111,7 @@ export interface FacetGeneratorOptions {
buildNumericFacet: CommerceFacetBuilder<NumericFacet>;
buildDateFacet: CommerceFacetBuilder<DateFacet>;
buildCategoryFacet: CommerceFacetBuilder<CategoryFacet>;
buildLocationFacet: CommerceFacetBuilder<LocationFacet>;
fetchProductsActionCreator: FetchProductsActionCreator;
}

Expand Down Expand Up @@ -159,6 +163,8 @@ export function buildFacetGenerator(
return options.buildNumericFacet(engine, {facetId});
case 'regular':
return options.buildRegularFacet(engine, {facetId});
case 'location':
return options.buildLocationFacet(engine, {facetId});
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import {FacetType} from '../../../../features/commerce/facets/facet-set/interfac
import {
AnyFacetRequest,
CategoryFacetValueRequest,
LocationFacetValueRequest,
} from '../../../../features/commerce/facets/facet-set/interfaces/request.js';
import {
AnyFacetResponse,
AnyFacetValueResponse,
CategoryFacetValue,
DateFacetValue,
LocationFacetValue,
NumericFacetValue,
RegularFacetValue,
} from '../../../../features/commerce/facets/facet-set/interfaces/response.js';
Expand All @@ -37,6 +39,8 @@ export type {
FacetType,
FacetValueRequest,
RegularFacetValue,
LocationFacetValueRequest,
LocationFacetValue,
NumericRangeRequest,
NumericFacetValue,
DateRangeRequest,
Expand Down
Loading

0 comments on commit 1dbd9a9

Please sign in to comment.