-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
facet(commerce): add commerce numeric facets (#3449)
- Loading branch information
1 parent
464b228
commit 2365788
Showing
35 changed files
with
2,562 additions
and
1,316 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
.../src/controllers/commerce/facets/core/generator/headless-commerce-facet-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import {commerceFacetSetReducer as commerceFacetSet} from '../../../../../features/commerce/facets/facet-set/facet-set-slice'; | ||
import {FacetType} from '../../../../../features/commerce/facets/facet-set/interfaces/response'; | ||
import {facetOrderReducer as facetOrder} from '../../../../../features/facets/facet-order/facet-order-slice'; | ||
import {buildMockCommerceEngine, MockCommerceEngine} from '../../../../../test'; | ||
import {buildMockCommerceFacetRequest} from '../../../../../test/mock-commerce-facet-request'; | ||
import { | ||
buildMockCommerceRegularFacetResponse, | ||
buildMockCommerceNumericFacetResponse, | ||
} from '../../../../../test/mock-commerce-facet-response'; | ||
import {buildMockCommerceState} from '../../../../../test/mock-commerce-state'; | ||
import {buildProductListingNumericFacet} from '../../../product-listing/facets/headless-product-listing-numeric-facet'; | ||
import {buildProductListingRegularFacet} from '../../../product-listing/facets/headless-product-listing-regular-facet'; | ||
import { | ||
buildCommerceFacetGenerator, | ||
CommerceFacetGenerator, | ||
CommerceFacetGeneratorOptions, | ||
} from './headless-commerce-facet-generator'; | ||
|
||
describe('CommerceFacetGenerator', () => { | ||
let engine: MockCommerceEngine; | ||
let options: CommerceFacetGeneratorOptions; | ||
let facetGenerator: CommerceFacetGenerator; | ||
|
||
function initFacetGenerator( | ||
facetId: string = 'regular_facet_id', | ||
type: FacetType = 'regular' | ||
) { | ||
const mockState = buildMockCommerceState(); | ||
const facets = []; | ||
switch (type) { | ||
case 'numericalRange': | ||
facets.push( | ||
buildMockCommerceNumericFacetResponse({ | ||
facetId, | ||
field: 'some_numeric_field', | ||
}) | ||
); | ||
break; | ||
case 'regular': | ||
case 'dateRange': // TODO | ||
case 'hierarchical': // TODO | ||
default: | ||
facets.push( | ||
buildMockCommerceRegularFacetResponse({ | ||
facetId, | ||
field: 'some_regular_field', | ||
}) | ||
); | ||
break; | ||
} | ||
engine = buildMockCommerceEngine({ | ||
state: { | ||
...mockState, | ||
productListing: { | ||
...mockState.productListing, | ||
facets: [ | ||
buildMockCommerceRegularFacetResponse({ | ||
facetId, | ||
field: 'some_regular_field', | ||
}), | ||
], | ||
}, | ||
facetOrder: [facetId], | ||
commerceFacetSet: { | ||
[facetId]: {request: buildMockCommerceFacetRequest({facetId, type})}, | ||
}, | ||
}, | ||
}); | ||
options = { | ||
buildNumericFacet: buildProductListingNumericFacet, | ||
buildRegularFacet: buildProductListingRegularFacet, | ||
}; | ||
facetGenerator = buildCommerceFacetGenerator(engine, options); | ||
} | ||
|
||
describe('upon initialization', () => { | ||
describe('regardless of the current facet state', () => { | ||
beforeEach(() => { | ||
initFacetGenerator(); | ||
}); | ||
|
||
it('initializes', () => { | ||
expect(facetGenerator).toBeTruthy(); | ||
}); | ||
|
||
it('adds correct reducers to engine', () => { | ||
expect(engine.addReducers).toHaveBeenCalledWith({ | ||
facetOrder, | ||
commerceFacetSet, | ||
}); | ||
}); | ||
|
||
it('exposes #subscribe method', () => { | ||
expect(facetGenerator.subscribe).toBeTruthy(); | ||
}); | ||
}); | ||
describe('when facet state contains regular facets', () => { | ||
it('should generate regular facet controllers', () => { | ||
const facetId = 'regular_facet_id'; | ||
initFacetGenerator(facetId, 'regular'); | ||
|
||
expect(facetGenerator.state.facets.length).toEqual(1); | ||
expect(facetGenerator.state.facets[0].state).toEqual( | ||
buildProductListingRegularFacet(engine, {facetId}).state | ||
); | ||
}); | ||
}); | ||
|
||
describe('when facet state contains numeric facets', () => { | ||
it('should generate numeric facet controllers', () => { | ||
const facetId = 'numeric_facet_id'; | ||
initFacetGenerator(facetId, 'numericalRange'); | ||
|
||
expect(facetGenerator.state.facets.length).toEqual(1); | ||
expect(facetGenerator.state.facets[0].state).toEqual( | ||
buildProductListingNumericFacet(engine, {facetId}).state | ||
); | ||
}); | ||
}); | ||
}); | ||
|
||
it('should generate date facet controllers', () => { | ||
// TODO | ||
}); | ||
|
||
it('should generate category facet controllers', () => { | ||
// TODO | ||
}); | ||
}); |
110 changes: 110 additions & 0 deletions
110
...dless/src/controllers/commerce/facets/core/generator/headless-commerce-facet-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import {CommerceEngine} from '../../../../../app/commerce-engine/commerce-engine'; | ||
import {commerceFacetSetReducer as commerceFacetSet} from '../../../../../features/commerce/facets/facet-set/facet-set-slice'; | ||
import {AnyFacetValueResponse} from '../../../../../features/commerce/facets/facet-set/interfaces/response'; | ||
import {facetOrderReducer as facetOrder} from '../../../../../features/facets/facet-order/facet-order-slice'; | ||
import {AnyFacetValueRequest} from '../../../../../features/facets/generic/interfaces/generic-facet-request'; | ||
import { | ||
CommerceFacetSetSection, | ||
FacetOrderSection, | ||
} from '../../../../../state/state-sections'; | ||
import {loadReducerError} from '../../../../../utils/errors'; | ||
import { | ||
buildController, | ||
Controller, | ||
} from '../../../../controller/headless-controller'; | ||
import {ProductListingNumericFacetBuilder} from '../../../product-listing/facets/headless-product-listing-numeric-facet'; | ||
import {ProductListingRegularFacetBuilder} from '../../../product-listing/facets/headless-product-listing-regular-facet'; | ||
import {CoreCommerceFacet} from '../headless-core-commerce-facet'; | ||
|
||
/** | ||
* The `CommerceFacetGenerator` headless controller creates commerce facet controllers from the Commerce API search or | ||
* product listing response. | ||
* | ||
* Commerce facets are not requested by the implementer, but rather pre-configured through the Coveo Merchandising Hub | ||
* (CMH). The implementer is only responsible for leveraging the facet controllers created by this controller to | ||
* properly render facets in their application. | ||
*/ | ||
export interface CommerceFacetGenerator extends Controller { | ||
/** | ||
* The state of the facet generator controller. | ||
*/ | ||
state: CommerceFacetGeneratorState; | ||
} | ||
|
||
/** | ||
* A scoped and simplified part of the headless state that is relevant to the facet generator controller. | ||
*/ | ||
export interface CommerceFacetGeneratorState { | ||
/** | ||
* The generated commerce facet controllers. | ||
*/ | ||
facets: CoreCommerceFacet<AnyFacetValueRequest, AnyFacetValueResponse>[]; | ||
} | ||
|
||
type CommerceRegularFacetBuilder = ProductListingRegularFacetBuilder; // TODO: | CommerceSearchRegularFacetBuilder; | ||
type CommerceNumericFacetBuilder = ProductListingNumericFacetBuilder; // TODO: | CommerceSearchNumericFacetBuilder; | ||
// TODO: type CommerceDateFacetBuilder = ProductListingDateFacetBuilder | CommerceSearchDateFacetBuilder; | ||
// TODO: type CommerceCategoryFacetBuilder = ProductListingCategoryFacetBuilder | CommerceSearchCategoryFacetBuilder; | ||
|
||
/** | ||
* @internal | ||
* | ||
* The `CommerceFacetGenerator` options used internally. | ||
*/ | ||
export interface CommerceFacetGeneratorOptions { | ||
buildRegularFacet: CommerceRegularFacetBuilder; | ||
buildNumericFacet: CommerceNumericFacetBuilder; | ||
// TODO: buildDateFacet: CommerceDateFacetBuilder; | ||
// TODO: buildCategoryFacet: CommerceNumericFacetBuilder; | ||
} | ||
|
||
/** | ||
* @internal | ||
* | ||
* Creates a `CommerceFacetGenerator` instance. | ||
* | ||
* @param engine - The headless commerce engine. | ||
* @param options - The facet generator options used internally. | ||
* @returns A `CommerceFacetGenerator` controller instance. | ||
*/ | ||
export function buildCommerceFacetGenerator( | ||
engine: CommerceEngine, | ||
options: CommerceFacetGeneratorOptions | ||
): CommerceFacetGenerator { | ||
if (!loadCommerceFacetGeneratorReducers(engine)) { | ||
throw loadReducerError; | ||
} | ||
|
||
const controller = buildController(engine); | ||
|
||
const createFacet = (facetId: string) => { | ||
const {type} = engine.state.commerceFacetSet[facetId].request; | ||
|
||
switch (type) { | ||
case 'numericalRange': | ||
return options.buildNumericFacet(engine, {facetId}); | ||
case 'dateRange': // TODO: return options.buildDateFacet(engine, {facetId}); | ||
case 'hierarchical': // TODO return options.buildCategoryFacet(engine, {facetId}); | ||
case 'regular': | ||
default: | ||
return options.buildRegularFacet(engine, {facetId}); | ||
} | ||
}; | ||
|
||
return { | ||
...controller, | ||
|
||
get state() { | ||
return { | ||
facets: engine.state.facetOrder.map(createFacet) ?? [], | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
function loadCommerceFacetGeneratorReducers( | ||
engine: CommerceEngine | ||
): engine is CommerceEngine<FacetOrderSection & CommerceFacetSetSection> { | ||
engine.addReducers({facetOrder, commerceFacetSet}); | ||
return true; | ||
} |
Oops, something went wrong.