-
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.
feat(commerce): extract core sort for plp and search (#3474)
* extract core sort for plp and search * fix documentation * update sort on search fulfilled * apply suggestions * clarify test group name * handle pagination reset consistently * use shared loader * Revert "handle pagination reset consistently" This reverts commit 25aa31a.
- Loading branch information
1 parent
74c3b65
commit 74c899a
Showing
14 changed files
with
423 additions
and
258 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
80 changes: 7 additions & 73 deletions
80
...dless/src/controllers/commerce/product-listing/sort/headless-product-listing-sort.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 |
---|---|---|
@@ -1,96 +1,30 @@ | ||
import {Action} from '@reduxjs/toolkit'; | ||
import {fetchProductListing} from '../../../../features/commerce/product-listing/product-listing-actions'; | ||
import {productListingV2Reducer} from '../../../../features/commerce/product-listing/product-listing-slice'; | ||
import {applySort} from '../../../../features/commerce/sort/sort-actions'; | ||
import {sortReducer} from '../../../../features/commerce/sort/sort-slice'; | ||
import {updatePage} from '../../../../features/pagination/pagination-actions'; | ||
import {buildMockCommerceEngine, MockCommerceEngine} from '../../../../test'; | ||
import {buildMockCommerceState} from '../../../../test/mock-commerce-state'; | ||
import { | ||
buildRelevanceSortCriterion, | ||
buildSort, | ||
Sort, | ||
SortBy, | ||
} from './headless-product-listing-sort'; | ||
} from '../../sort/core/headless-core-commerce-sort'; | ||
import {buildProductListingSort} from './headless-product-listing-sort'; | ||
|
||
describe('headless product-listing-sort', () => { | ||
describe('headless product listing sort', () => { | ||
let sort: Sort; | ||
let engine: MockCommerceEngine; | ||
|
||
beforeEach(() => { | ||
engine = buildMockCommerceEngine(); | ||
sort = buildSort(engine); | ||
sort = buildProductListingSort(engine); | ||
}); | ||
|
||
const expectContainAction = (action: Action) => { | ||
const found = engine.actions.find((a) => a.type === action.type); | ||
expect(engine.actions).toContainEqual(found); | ||
}; | ||
|
||
it('adds the correct reducers to engine', () => { | ||
expect(engine.addReducers).toHaveBeenCalledWith({ | ||
productListing: productListingV2Reducer, | ||
commerceSort: sortReducer, | ||
}); | ||
}); | ||
|
||
it('dispatches #applySort on load when sort is specified', () => { | ||
sort = buildSort(engine, { | ||
initialState: { | ||
criterion: buildRelevanceSortCriterion(), | ||
}, | ||
}); | ||
expectContainAction(applySort); | ||
}); | ||
|
||
it('sortBy dispatches #applySort, #updatePage, and #fetchProductListing', () => { | ||
it('#sortBy dispatches #fetchProductListing', () => { | ||
sort.sortBy(buildRelevanceSortCriterion()); | ||
expectContainAction(applySort); | ||
expectContainAction(updatePage); | ||
expectContainAction(fetchProductListing.pending); | ||
}); | ||
|
||
describe('when sort is populated', () => { | ||
const appliedSort = { | ||
by: SortBy.Fields, | ||
fields: [{name: 'some_field'}], | ||
}; | ||
|
||
beforeEach(() => { | ||
engine = buildMockCommerceEngine({ | ||
state: { | ||
...buildMockCommerceState(), | ||
commerceSort: { | ||
appliedSort: appliedSort, | ||
availableSorts: [appliedSort], | ||
}, | ||
}, | ||
}); | ||
sort = buildSort(engine); | ||
}); | ||
|
||
it('calling #isSortedBy with a criterion equal to the one in state returns true', () => { | ||
expect(sort.isSortedBy(appliedSort)).toBe(true); | ||
}); | ||
|
||
it('calling #isSortedBy with a criterion different from the one in state returns false', () => { | ||
const notAppliedSort = { | ||
by: SortBy.Fields, | ||
fields: [{name: 'some_other_field'}], | ||
}; | ||
expect(sort.isSortedBy(notAppliedSort)).toBe(false); | ||
}); | ||
|
||
it('calling #isAvailable with an available criterion returns true', () => { | ||
expect(sort.isAvailable(appliedSort)).toBe(true); | ||
}); | ||
|
||
it('calling #isAvailable with an unavailable criterion returns false', () => { | ||
const unavailableSort = { | ||
by: SortBy.Fields, | ||
fields: [{name: 'some_other_field'}], | ||
}; | ||
expect(sort.isAvailable(unavailableSort)).toBe(false); | ||
}); | ||
const action = engine.findAsyncAction(fetchProductListing.pending); | ||
expect(action).toBeTruthy(); | ||
}); | ||
}); |
162 changes: 15 additions & 147 deletions
162
...s/headless/src/controllers/commerce/product-listing/sort/headless-product-listing-sort.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 |
---|---|---|
@@ -1,162 +1,30 @@ | ||
import {Schema} from '@coveo/bueno'; | ||
import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine'; | ||
import {CoreEngine} from '../../../../app/engine'; | ||
import {fetchProductListing} from '../../../../features/commerce/product-listing/product-listing-actions'; | ||
import {productListingV2Reducer as productListing} from '../../../../features/commerce/product-listing/product-listing-slice'; | ||
import { | ||
buildFieldsSortCriterion, | ||
buildRelevanceSortCriterion, | ||
SortBy, | ||
SortByFields, | ||
SortByFieldsFields, | ||
SortByRelevance, | ||
SortCriterion, | ||
SortDirection, | ||
sortCriterionDefinition, | ||
} from '../../../../features/commerce/sort/sort'; | ||
import {applySort} from '../../../../features/commerce/sort/sort-actions'; | ||
import {sortReducer as commerceSort} from '../../../../features/commerce/sort/sort-slice'; | ||
import {updatePage} from '../../../../features/pagination/pagination-actions'; | ||
import {ProductListingV2Section} from '../../../../state/state-sections'; | ||
import {loadReducerError} from '../../../../utils/errors'; | ||
import {validateInitialState} from '../../../../utils/validate-payload'; | ||
import { | ||
buildController, | ||
Controller, | ||
} from '../../../controller/headless-controller'; | ||
|
||
export type {SortByRelevance, SortByFields, SortByFieldsFields, SortCriterion}; | ||
export { | ||
SortBy, | ||
SortDirection, | ||
buildRelevanceSortCriterion, | ||
buildFieldsSortCriterion, | ||
}; | ||
|
||
export interface SortProps { | ||
/** | ||
* The initial state that should be applied to this `Sort` controller. | ||
*/ | ||
initialState?: SortInitialState; | ||
} | ||
|
||
export interface SortInitialState { | ||
/** | ||
* The initial sort criterion to register in state. | ||
*/ | ||
criterion?: SortCriterion; | ||
} | ||
|
||
function validateSortInitialState( | ||
engine: CoreEngine<ProductListingV2Section>, | ||
state: SortInitialState | undefined | ||
) { | ||
if (!state) { | ||
return; | ||
} | ||
|
||
const schema = new Schema<SortInitialState>({ | ||
criterion: sortCriterionDefinition, | ||
}); | ||
|
||
validateInitialState(engine, schema, state, 'buildSort'); | ||
} | ||
|
||
export interface Sort extends Controller { | ||
/** | ||
* Updates the sort criterion and executes a new query. | ||
* | ||
* @param criterion - The new sort criterion. | ||
*/ | ||
sortBy(criterion: SortCriterion): void; | ||
|
||
/** | ||
* Checks whether the specified sort criterion matches the value in state. | ||
* | ||
* @param criterion - The criterion to compare. | ||
* @returns `true` if the passed sort criterion matches the value in state, and `false` otherwise. | ||
*/ | ||
isSortedBy(criterion: SortCriterion): boolean; | ||
|
||
/** | ||
* Checks whether the specified sort criterion is available. | ||
* | ||
* @param criterion - The criterion to check for. | ||
* @returns `true` if the passed sort criterion is available, and `false` otherwise. | ||
*/ | ||
isAvailable(criterion: SortCriterion): boolean; | ||
|
||
/** | ||
* A scoped and simplified part of the headless state that is relevant to the `Sort` controller. | ||
*/ | ||
state: SortState; | ||
} | ||
|
||
export interface SortState { | ||
/** | ||
* The current sort criterion. | ||
*/ | ||
appliedSort: SortCriterion; | ||
|
||
/** | ||
* The available sort criteria. | ||
*/ | ||
availableSorts: SortCriterion[]; | ||
} | ||
Sort, | ||
buildCoreSort, | ||
SortProps, | ||
} from '../../sort/core/headless-core-commerce-sort'; | ||
import {loadProductListingReducer} from '../utils/load-product-listing-reducers'; | ||
|
||
/** | ||
* Creates a `Sort` controller instance for the product listing. | ||
* | ||
* @param engine - The headless engine. | ||
* @param engine - The headless commerce engine. | ||
* @param props - The configurable `Sort` controller properties. | ||
* @returns A `Sort` controller instance. | ||
*/ | ||
export function buildSort(engine: CommerceEngine, props: SortProps = {}): Sort { | ||
if (!loadSortReducers(engine)) { | ||
export function buildProductListingSort( | ||
engine: CommerceEngine, | ||
props: SortProps = {} | ||
): Sort { | ||
if (!loadProductListingReducer(engine)) { | ||
throw loadReducerError; | ||
} | ||
|
||
const {dispatch} = engine; | ||
const controller = buildController(engine); | ||
const getState = () => engine.state; | ||
|
||
validateSortInitialState(engine, props.initialState); | ||
|
||
const criterion = props.initialState?.criterion; | ||
|
||
if (criterion) { | ||
dispatch(applySort(criterion)); | ||
} | ||
|
||
return { | ||
...controller, | ||
|
||
get state() { | ||
return getState().commerceSort; | ||
}, | ||
|
||
sortBy(criterion: SortCriterion) { | ||
dispatch(applySort(criterion)); | ||
dispatch(updatePage(0)); | ||
dispatch(fetchProductListing()); | ||
}, | ||
|
||
isSortedBy(criterion: SortCriterion) { | ||
return ( | ||
JSON.stringify(this.state.appliedSort) === JSON.stringify(criterion) | ||
); | ||
}, | ||
|
||
isAvailable(criterion: SortCriterion) { | ||
return this.state.availableSorts.some( | ||
(availableCriterion) => | ||
JSON.stringify(availableCriterion) === JSON.stringify(criterion) | ||
); | ||
}, | ||
}; | ||
} | ||
|
||
function loadSortReducers(engine: CommerceEngine): engine is CommerceEngine { | ||
engine.addReducers({productListing, commerceSort}); | ||
return true; | ||
return buildCoreSort(engine, { | ||
...props, | ||
fetchResultsActionCreator: fetchProductListing, | ||
}); | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/headless/src/controllers/commerce/search/sort/headless-search-sort.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,30 @@ | ||
import {executeSearch} from '../../../../features/commerce/search/search-actions'; | ||
import {commerceSearchReducer as commerceSearch} from '../../../../features/commerce/search/search-slice'; | ||
import {buildMockCommerceEngine, MockCommerceEngine} from '../../../../test'; | ||
import { | ||
buildRelevanceSortCriterion, | ||
Sort, | ||
} from '../../sort/core/headless-core-commerce-sort'; | ||
import {buildSearchSort} from './headless-search-sort'; | ||
|
||
describe('commerce search sort', () => { | ||
let sort: Sort; | ||
let engine: MockCommerceEngine; | ||
|
||
beforeEach(() => { | ||
engine = buildMockCommerceEngine(); | ||
sort = buildSearchSort(engine); | ||
}); | ||
|
||
it('adds the correct reducers to engine', () => { | ||
expect(engine.addReducers).toHaveBeenCalledWith({ | ||
commerceSearch, | ||
}); | ||
}); | ||
|
||
it('#sortBy dispatches #fetchProductListing', () => { | ||
sort.sortBy(buildRelevanceSortCriterion()); | ||
const action = engine.findAsyncAction(executeSearch.pending); | ||
expect(action).toBeTruthy(); | ||
}); | ||
}); |
30 changes: 30 additions & 0 deletions
30
packages/headless/src/controllers/commerce/search/sort/headless-search-sort.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,30 @@ | ||
import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine'; | ||
import {executeSearch} from '../../../../features/commerce/search/search-actions'; | ||
import {loadReducerError} from '../../../../utils/errors'; | ||
import { | ||
Sort, | ||
buildCoreSort, | ||
SortProps, | ||
} from '../../sort/core/headless-core-commerce-sort'; | ||
import {loadSearchReducer} from '../utils/headless-search-reducers'; | ||
|
||
/** | ||
* Creates a `Sort` controller instance for commerce search. | ||
* | ||
* @param engine - The headless commerce engine. | ||
* @param props - The configurable `Sort` controller properties. | ||
* @returns A `Sort` controller instance. | ||
*/ | ||
export function buildSearchSort( | ||
engine: CommerceEngine, | ||
props: SortProps = {} | ||
): Sort { | ||
if (!loadSearchReducer(engine)) { | ||
throw loadReducerError; | ||
} | ||
|
||
return buildCoreSort(engine, { | ||
...props, | ||
fetchResultsActionCreator: executeSearch, | ||
}); | ||
} |
Oops, something went wrong.