From d3338121f6e49477424faec1d520c9ac53e11af3 Mon Sep 17 00:00:00 2001 From: Nicholas Labarre Date: Thu, 27 Jun 2024 17:06:30 -0400 Subject: [PATCH] fix(commerce): expose child product directly on `promoteChildToParent` (#4128) Instead of mutating the received products from the API, I propose we dynamically find the corresponding parent and children in the reducer. This _seems_ less error-prone to me, because we don't need to mutate products in every place we fulfill a request containing `products`, at the cost of being less performant, as we iterate on every product every time a children is promoted. Thoughts? An other option to consider would be to mutate the products directly in the commerce api client. https://coveord.atlassian.net/browse/KIT-3221 --- .../atomic-commerce-interface/store.ts | 5 +-- .../atomic-commerce-product-list.tsx | 9 ++---- .../store.ts | 5 +-- .../atomic-commerce-recommendation-list.tsx | 6 +--- .../atomic-commerce-search-box.tsx | 6 +--- .../atomic-product-children.tsx | 21 +++++-------- ...c-commerce-search-box-instant-products.tsx | 9 +++--- .../headless-instant-products.test.ts | 11 ++++--- .../headless-instant-products.ts | 15 +++------ .../headless-product-listing.test.ts | 9 +++--- .../headless-product-listing.ts | 14 +++------ .../headless-recommendations.test.ts | 9 +++--- .../headless-recommendations.ts | 16 +++------- .../commerce/search/headless-search.test.ts | 11 +++---- .../commerce/search/headless-search.ts | 14 +++------ .../instant-products-actions.ts | 14 ++++++--- .../instant-products-slice.test.ts | 8 ++--- .../instant-products-slice.ts | 29 +++++++++-------- .../product-listing-actions.ts | 14 ++++++--- .../product-listing-slice.test.ts | 8 ++--- .../product-listing/product-listing-slice.ts | 31 +++++++++---------- .../recommendations-actions.ts | 14 ++++++--- .../recommendations-slice.test.ts | 8 ++--- .../recommendations/recommendations-slice.ts | 31 +++++++++---------- .../commerce/search/search-actions.ts | 14 ++++++--- .../commerce/search/search-slice.test.ts | 8 ++--- .../features/commerce/search/search-slice.ts | 31 +++++++++---------- 27 files changed, 172 insertions(+), 198 deletions(-) diff --git a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts index c3f6e0b78a2..04163936a07 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts @@ -4,6 +4,7 @@ import { NumericFacetValue, DateFacetValue, SortCriterion, + ChildProduct, } from '@coveo/headless/commerce'; import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; import { @@ -32,7 +33,7 @@ export interface AtomicStoreData extends AtomicCommonStoreData { sortOptions: SortDropdownOption[]; mobileBreakpoint: string; currentQuickviewPosition: number; - activeProductChild: {parentPermanentId: string; childPermanentId: string}; + activeProductChild: ChildProduct | undefined; } export interface AtomicCommerceStore @@ -63,7 +64,7 @@ export function createAtomicCommerceStore( mobileBreakpoint: DEFAULT_MOBILE_BREAKPOINT, fieldsToInclude: [], currentQuickviewPosition: -1, - activeProductChild: {parentPermanentId: '', childPermanentId: ''}, + activeProductChild: undefined, }); return { diff --git a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx index 3fe1aa077f6..6da6a7a72a1 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx @@ -174,15 +174,12 @@ export class AtomicCommerceProductList @Listen('atomic/selectChildProduct') public onSelectChildProduct(event: CustomEvent) { event.stopPropagation(); - const {parentPermanentId, childPermanentId} = event.detail; + const child = event.detail.child; if (this.bindings.interfaceElement.type === 'product-listing') { - this.productListing.promoteChildToParent( - childPermanentId, - parentPermanentId - ); + this.productListing.promoteChildToParent(child); } else if (this.bindings.interfaceElement.type === 'search') { - this.search.promoteChildToParent(childPermanentId, parentPermanentId); + this.search.promoteChildToParent(child); } } diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts index f66270eacd6..371ae7bd9b2 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '@coveo/headless/commerce'; import {DEFAULT_MOBILE_BREAKPOINT} from '../../../utils/replace-breakpoint'; import { AtomicCommonStore, @@ -9,7 +10,7 @@ import {makeDesktopQuery} from '../atomic-commerce-layout/commerce-layout'; export interface AtomicStoreData extends AtomicCommonStoreData { mobileBreakpoint: string; currentQuickviewPosition: number; - activeProductChild: {parentPermanentId: string; childPermanentId: string}; + activeProductChild: ChildProduct | undefined; } export interface AtomicCommerceRecommendationStore @@ -29,7 +30,7 @@ export function createAtomicCommerceRecommendationStore(): AtomicCommerceRecomme mobileBreakpoint: DEFAULT_MOBILE_BREAKPOINT, fieldsToInclude: [], currentQuickviewPosition: -1, - activeProductChild: {parentPermanentId: '', childPermanentId: ''}, + activeProductChild: undefined, }); return { diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx index a66c8c58f75..010f5e6f099 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx @@ -201,12 +201,8 @@ export class AtomicCommerceRecommendationList @Listen('atomic/selectChildProduct') public onSelectChildProduct(event: CustomEvent) { event.stopPropagation(); - const {parentPermanentId, childPermanentId} = event.detail; - this.recommendations.promoteChildToParent( - childPermanentId, - parentPermanentId - ); + this.recommendations.promoteChildToParent(event.detail.child); } public get focusTarget() { diff --git a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.tsx b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.tsx index cb6b3b89443..4dd42889d63 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-search-box/atomic-commerce-search-box.tsx @@ -267,11 +267,7 @@ export class AtomicCommerceSearchBox @Listen('atomic/selectChildProduct') public onSelectChildProduct(event: CustomEvent) { event.stopPropagation(); - const {parentPermanentId, childPermanentId} = event.detail; - this.bindings.store.state.activeProductChild = { - parentPermanentId, - childPermanentId, - }; + this.bindings.store.state.activeProductChild = event.detail.child; this.suggestionManager.forceUpdate(); } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx index aa234204ab7..43f1e5ca7e2 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx @@ -22,8 +22,7 @@ import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce- import {ProductContext} from '../product-template-decorators'; export interface SelectChildProductEventArgs { - childPermanentId: string; - parentPermanentId: string; + child: ChildProduct; } /** @@ -98,11 +97,10 @@ export class AtomicProductChildren }); } - private onSelectChild(childPermanentId: string, parentPermanentId: string) { - this.activeChildId = childPermanentId; + private onSelectChild(child: ChildProduct) { + this.activeChildId = child.permanentid; this.selectChildProduct.emit({ - childPermanentId, - parentPermanentId, + child, }); } @@ -131,15 +129,10 @@ export class AtomicProductChildren class={`product-child${child.permanentid === this.activeChildId ? this.activeChildClasses : ' '}`} title={child.ec_name || undefined} onKeyPress={(event) => - event.key === 'Enter' && - this.onSelectChild(child.permanentid, this.product.permanentid) - } - onMouseEnter={() => - this.onSelectChild(child.permanentid, this.product.permanentid) - } - onTouchStart={() => - this.onSelectChild(child.permanentid, this.product.permanentid) + event.key === 'Enter' && this.onSelectChild(child) } + onMouseEnter={() => this.onSelectChild(child)} + onTouchStart={() => this.onSelectChild(child)} > { - this.instantProducts.promoteChildToParent( - this.bindings.store.state.activeProductChild.childPermanentId, - this.bindings.store.state.activeProductChild.parentPermanentId - ); + if (this.bindings.store.state.activeProductChild) { + this.instantProducts.promoteChildToParent( + this.bindings.store.state.activeProductChild + ); + } }); this.itemTemplateProvider = new ProductTemplateProvider({ diff --git a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.test.ts b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.test.ts index 7aa4e214314..4b31577d1dc 100644 --- a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.test.ts +++ b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {stateKey} from '../../../app/state-key'; import { registerInstantProducts, @@ -46,8 +47,9 @@ describe('instant products', () => { // TODO KIT-3210 test #updateQuery, #clearExpired, and #state it('#promoteChildToParent dispatches #promoteChildToParent with the correct arguments', () => { - const childPermanentId = 'childPermanentId'; - const parentPermanentId = 'parentPermanentId'; + const child = { + permanentid: 'childPermanentId', + } as ChildProduct; const query = 'query'; engine[stateKey].instantProducts![searchBoxId] = { @@ -56,11 +58,10 @@ describe('instant products', () => { }; instantProducts = buildInstantProducts(engine, {options: {searchBoxId}}); - instantProducts.promoteChildToParent(childPermanentId, parentPermanentId); + instantProducts.promoteChildToParent(child); expect(promoteChildToParent).toHaveBeenCalledWith({ - childPermanentId, - parentPermanentId, + child, id: searchBoxId, query, }); diff --git a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ts b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ts index 05d6858ebf7..fe5f4be0cb4 100644 --- a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ts +++ b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ts @@ -1,7 +1,7 @@ import {NumberValue, Schema} from '@coveo/bueno'; import {SerializedError} from '@reduxjs/toolkit'; import {CommerceAPIErrorResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product} from '../../../api/commerce/common/product'; +import {ChildProduct, Product} from '../../../api/commerce/common/product'; import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine'; import {stateKey} from '../../../app/state-key'; import { @@ -78,13 +78,9 @@ export interface InstantProducts extends Controller { * **Note:** In the controller state, a product that has children will always include itself as its own child so that * it can be rendered as a nested product, and restored as the parent product through this method as needed. * - * @param childPermanentId The permanentid of the child product that will become the new parent. - * @param parentPermanentId The permanentid of the current parent product of the child product to promote. + * @param child The child product that will become the new parent. */ - promoteChildToParent( - childPermanentId: string, - parentPermanentId: string - ): void; + promoteChildToParent(child: ChildProduct): void; /** * Creates an `InteractiveProduct` sub-controller. @@ -198,11 +194,10 @@ export function buildInstantProducts( ); }, - promoteChildToParent(childPermanentId, parentPermanentId) { + promoteChildToParent(child: ChildProduct) { dispatch( promoteChildToParent({ - childPermanentId, - parentPermanentId, + child, id: searchBoxId, query: getQuery(), }) diff --git a/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.test.ts b/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.test.ts index 5465fdba6fa..409c9b0d880 100644 --- a/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.test.ts +++ b/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {configuration} from '../../../app/common-reducers'; import {contextReducer} from '../../../features/commerce/context/context-slice'; import { @@ -88,14 +89,12 @@ describe('headless product-listing', () => { ProductListingActions, 'promoteChildToParent' ); - const childPermanentId = 'childPermanentId'; - const parentPermanentId = 'parentPermanentId'; + const child = {permanentid: 'childPermanentId'} as ChildProduct; - productListing.promoteChildToParent(childPermanentId, parentPermanentId); + productListing.promoteChildToParent(child); expect(promoteChildToParent).toHaveBeenCalledWith({ - childPermanentId, - parentPermanentId, + child, }); }); diff --git a/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.ts b/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.ts index 15b14602a19..b842565f8d1 100644 --- a/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.ts +++ b/packages/headless/src/controllers/commerce/product-listing/headless-product-listing.ts @@ -1,5 +1,5 @@ import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product} from '../../../api/commerce/common/product'; +import {ChildProduct, Product} from '../../../api/commerce/common/product'; import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine'; import {configuration} from '../../../app/common-reducers'; import {stateKey} from '../../../app/state-key'; @@ -74,13 +74,9 @@ export interface ProductListing * **Note:** In the controller state, a product that has children will always include itself as its own child so that * it can be rendered as a nested product, and restored as the parent product through this method as needed. * - * @param childPermanentId The permanentid of the child product that will become the new parent. - * @param parentPermanentId The permanentid of the current parent product of the child product to promote. + * @param child The child product that will become the new parent. */ - promoteChildToParent( - childPermanentId: string, - parentPermanentId: string - ): void; + promoteChildToParent(child: ChildProduct): void; /** * A scoped and simplified part of the headless state that is relevant to the `ProductListing` controller. @@ -144,8 +140,8 @@ export function buildProductListing(engine: CommerceEngine): ProductListing { }; }, - promoteChildToParent(childPermanentId, parentPermanentId) { - dispatch(promoteChildToParent({childPermanentId, parentPermanentId})); + promoteChildToParent(child: ChildProduct) { + dispatch(promoteChildToParent({child})); }, refresh: () => dispatch(fetchProductListing()), diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts index 310363414ad..2716dba146d 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import { fetchRecommendations, promoteChildToParent, @@ -33,14 +34,12 @@ describe('headless recommendations', () => { }); it('#promoteChildToParent dispatches #promoteChildToParent with the correct arguments', () => { - const childPermanentId = 'childPermanentId'; - const parentPermanentId = 'parentPermanentId'; + const child = {permanentid: 'childPermanentId'} as ChildProduct; - recommendations.promoteChildToParent(childPermanentId, parentPermanentId); + recommendations.promoteChildToParent(child); expect(promoteChildToParent).toHaveBeenCalledWith({ - childPermanentId, - parentPermanentId, + child, slotId: 'slot-id', }); }); diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts index 6ec1cc817db..8c9b667c11a 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts @@ -1,6 +1,6 @@ import {createSelector} from '@reduxjs/toolkit'; import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product} from '../../../api/commerce/common/product'; +import {ChildProduct, Product} from '../../../api/commerce/common/product'; import { CommerceEngine, CommerceEngineState, @@ -60,13 +60,9 @@ export interface Recommendations * **Note:** In the controller state, a product that has children will always include itself as its own child so that * it can be rendered as a nested product, and restored as the parent product through this method as needed. * - * @param childPermanentId The permanentid of the child product that will become the new parent. - * @param parentPermanentId The permanentid of the current parent product of the child product to promote. + * @param child The child product that will become the new parent. */ - promoteChildToParent( - childPermanentId: string, - parentPermanentId: string - ): void; + promoteChildToParent(child: ChildProduct): void; /** * A scoped and simplified part of the headless state that is relevant to the `Recommendations` controller. @@ -154,10 +150,8 @@ export function buildRecommendations( ...controller, ...subControllers, - promoteChildToParent(childPermanentId, parentPermanentId) { - dispatch( - promoteChildToParent({childPermanentId, parentPermanentId, slotId}) - ); + promoteChildToParent(child: ChildProduct) { + dispatch(promoteChildToParent({child, slotId})); }, get state() { diff --git a/packages/headless/src/controllers/commerce/search/headless-search.test.ts b/packages/headless/src/controllers/commerce/search/headless-search.test.ts index c3a618703ff..723f4f57384 100644 --- a/packages/headless/src/controllers/commerce/search/headless-search.test.ts +++ b/packages/headless/src/controllers/commerce/search/headless-search.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {configuration} from '../../../app/common-reducers'; import {contextReducer as commerceContext} from '../../../features/commerce/context/context-slice'; import { @@ -90,15 +91,11 @@ describe('headless search', () => { SearchActions, 'promoteChildToParent' ); - const childPermanentId = 'childPermanentId'; - const parentPermanentId = 'parentPermanentId'; + const child = {permanentid: 'childPermanentId'} as ChildProduct; - search.promoteChildToParent(childPermanentId, parentPermanentId); + search.promoteChildToParent(child); - expect(promoteChildToParent).toHaveBeenCalledWith({ - childPermanentId, - parentPermanentId, - }); + expect(promoteChildToParent).toHaveBeenCalledWith({child}); }); it('executeFirstSearch dispatches #executeSearch', () => { diff --git a/packages/headless/src/controllers/commerce/search/headless-search.ts b/packages/headless/src/controllers/commerce/search/headless-search.ts index ecd49575fdd..08629248b9f 100644 --- a/packages/headless/src/controllers/commerce/search/headless-search.ts +++ b/packages/headless/src/controllers/commerce/search/headless-search.ts @@ -1,5 +1,5 @@ import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product} from '../../../api/commerce/common/product'; +import {ChildProduct, Product} from '../../../api/commerce/common/product'; import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine'; import {configuration} from '../../../app/common-reducers'; import {stateKey} from '../../../app/state-key'; @@ -62,13 +62,9 @@ export interface Search extends Controller, SearchSubControllers { * **Note:** In the controller state, a product that has children will always include itself as its own child so that * it can be rendered as a nested product, and restored as the parent product through this method as needed. * - * @param childPermanentId The permanentid of the child product that will become the new parent. - * @param parentPermanentId The permanentid of the current parent product of the child product to promote. + * @param child The child product that will become the new parent. */ - promoteChildToParent( - childPermanentId: string, - parentPermanentId: string - ): void; + promoteChildToParent(child: ChildProduct): void; /** * A scoped and simplified part of the headless state that is relevant to the `Search` controller. @@ -125,8 +121,8 @@ export function buildSearch(engine: CommerceEngine): Search { return getState().commerceSearch; }, - promoteChildToParent(childPermanentId: string, parentPermanentId: string) { - dispatch(promoteChildToParent({childPermanentId, parentPermanentId})); + promoteChildToParent(child: ChildProduct) { + dispatch(promoteChildToParent({child})); }, executeFirstSearch() { diff --git a/packages/headless/src/features/commerce/instant-products/instant-products-actions.ts b/packages/headless/src/features/commerce/instant-products/instant-products-actions.ts index 1e82288a42a..cea169bad9c 100644 --- a/packages/headless/src/features/commerce/instant-products/instant-products-actions.ts +++ b/packages/headless/src/features/commerce/instant-products/instant-products-actions.ts @@ -1,5 +1,6 @@ -import {StringValue} from '@coveo/bueno'; +import {RecordValue, StringValue} from '@coveo/bueno'; import {createAction} from '@reduxjs/toolkit'; +import {ChildProduct} from '../../../api/commerce/common/product'; import { validatePayload, requiredEmptyAllowedString, @@ -56,13 +57,16 @@ export const clearExpiredProducts = createAction( export interface PromoteChildToParentActionCreatorPayload extends UpdateInstantProductQueryActionCreatorPayload { - childPermanentId: string; - parentPermanentId: string; + child: ChildProduct; } export const promoteChildToParentDefinition = { - childPermanentId: new StringValue({required: true}), - parentPermanentId: new StringValue({required: true}), + child: new RecordValue({ + options: {required: true}, + values: { + permanentid: new StringValue({required: true}), + }, + }), ...instantProductsQueryDefinition, }; diff --git a/packages/headless/src/features/commerce/instant-products/instant-products-slice.test.ts b/packages/headless/src/features/commerce/instant-products/instant-products-slice.test.ts index 16d22e9c699..2275a703f58 100644 --- a/packages/headless/src/features/commerce/instant-products/instant-products-slice.test.ts +++ b/packages/headless/src/features/commerce/instant-products/instant-products-slice.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {SearchCommerceSuccessResponse} from '../../../api/commerce/search/response'; import { buildMockChildProduct, @@ -340,7 +341,7 @@ describe('instant products slice', () => { }); describe('on #promoteChildToParent', () => { - const childPermanentId = 'child-id'; + const permanentid = 'child-id'; const parentPermanentId = 'parent-id'; const id: string = id1; const query = 'some_query'; @@ -349,8 +350,7 @@ describe('instant products slice', () => { beforeEach(() => { action = promoteChildToParent({ - childPermanentId, - parentPermanentId, + child: {permanentid} as ChildProduct, id, query, }); @@ -395,7 +395,7 @@ describe('instant products slice', () => { it('when both parent and child exist in cache for query, promotes the child to parent', () => { const childProduct = buildMockChildProduct({ - permanentid: childPermanentId, + permanentid, additionalFields: {test: 'test'}, clickUri: 'child-uri', ec_brand: 'child brand', diff --git a/packages/headless/src/features/commerce/instant-products/instant-products-slice.ts b/packages/headless/src/features/commerce/instant-products/instant-products-slice.ts index c850e35048e..d3059ea1cf1 100644 --- a/packages/headless/src/features/commerce/instant-products/instant-products-slice.ts +++ b/packages/headless/src/features/commerce/instant-products/instant-products-slice.ts @@ -1,5 +1,9 @@ import {createReducer} from '@reduxjs/toolkit'; -import {Product, BaseProduct} from '../../../api/commerce/common/product'; +import { + Product, + BaseProduct, + ChildProduct, +} from '../../../api/commerce/common/product'; import { clearExpiredItems, fetchItemsFulfilled, @@ -69,28 +73,23 @@ export const instantProductsReducer = createReducer( } const products = cache.products; - const currentParentIndex = products.findIndex( - (product) => product.permanentid === action.payload.parentPermanentId - ); + let childToPromote; + const currentParentIndex = products.findIndex((product) => { + childToPromote = product.children.find( + (child) => child.permanentid === action.payload.child.permanentid + ); + return !!childToPromote; + }); - if (currentParentIndex === -1) { + if (currentParentIndex === -1 || childToPromote === undefined) { return; } const position = products[currentParentIndex].position; - const {children, totalNumberOfChildren} = products[currentParentIndex]; - const childToPromote = children.find( - (child) => child.permanentid === action.payload.childPermanentId - ); - - if (childToPromote === undefined) { - return; - } - const newParent: Product = { - ...childToPromote, + ...(childToPromote as ChildProduct), children, totalNumberOfChildren, position, diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts b/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts index b23bc034b4e..487cb460024 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts @@ -1,9 +1,10 @@ -import {StringValue} from '@coveo/bueno'; +import {RecordValue, StringValue} from '@coveo/bueno'; import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; import { AsyncThunkCommerceOptions, isErrorResponse, } from '../../../api/commerce/commerce-api-client'; +import {ChildProduct} from '../../../api/commerce/common/product'; import {ProductListingSection} from '../../../state/state-sections'; import {validatePayload} from '../../../utils/validate-payload'; import {logQueryError} from '../../search/search-analytics-actions'; @@ -93,13 +94,16 @@ export const fetchMoreProducts = createAsyncThunk< ); export interface PromoteChildToParentActionCreatorPayload { - childPermanentId: string; - parentPermanentId: string; + child: ChildProduct; } export const promoteChildToParentDefinition = { - childPermanentId: new StringValue({required: true}), - parentPermanentId: new StringValue({required: true}), + child: new RecordValue({ + options: {required: true}, + values: { + permanentid: new StringValue({required: true}), + }, + }), }; export const promoteChildToParent = createAction( diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts b/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts index 35174d4fb1f..d231ee03ba6 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {buildMockCommerceRegularFacetResponse} from '../../../test/mock-commerce-facet-response'; import { buildMockChildProduct, @@ -245,14 +246,13 @@ describe('product-listing-slice', () => { }); describe('on #promoteChildToParent', () => { - const childPermanentId = 'child-id'; + const permanentid = 'child-id'; const parentPermanentId = 'parent-id'; let action: ReturnType; beforeEach(() => { action = promoteChildToParent({ - childPermanentId, - parentPermanentId, + child: {permanentid} as ChildProduct, }); }); @@ -274,7 +274,7 @@ describe('product-listing-slice', () => { it('when both parent and child exist, promotes the child to parent', () => { const childProduct = buildMockChildProduct({ - permanentid: childPermanentId, + permanentid, additionalFields: {test: 'test'}, clickUri: 'child-uri', ec_brand: 'child brand', diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-slice.ts b/packages/headless/src/features/commerce/product-listing/product-listing-slice.ts index 9e0b635a1cd..72b95dceee7 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-slice.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-slice.ts @@ -1,6 +1,10 @@ import {createReducer} from '@reduxjs/toolkit'; import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product, BaseProduct} from '../../../api/commerce/common/product'; +import { + Product, + BaseProduct, + ChildProduct, +} from '../../../api/commerce/common/product'; import {CommerceSuccessResponse} from '../../../api/commerce/common/response'; import {QueryCommerceAPIThunkReturn} from '../common/actions'; import { @@ -52,28 +56,23 @@ export const productListingReducer = createReducer( }) .addCase(promoteChildToParent, (state, action) => { const {products} = state; - const currentParentIndex = products.findIndex( - (product) => product.permanentid === action.payload.parentPermanentId - ); - - if (currentParentIndex === -1) { + let childToPromote; + const currentParentIndex = products.findIndex((product) => { + childToPromote = product.children.find( + (child) => child.permanentid === action.payload.child.permanentid + ); + return !!childToPromote; + }); + + if (currentParentIndex === -1 || childToPromote === undefined) { return; } const position = products[currentParentIndex].position; - const {children, totalNumberOfChildren} = products[currentParentIndex]; - const childToPromote = children.find( - (child) => child.permanentid === action.payload.childPermanentId - ); - - if (childToPromote === undefined) { - return; - } - const newParent: Product = { - ...childToPromote, + ...(childToPromote as ChildProduct), children, totalNumberOfChildren, position, diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts b/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts index 744551df20a..6a374e4ef5c 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts @@ -1,10 +1,11 @@ -import {StringValue} from '@coveo/bueno'; +import {RecordValue, StringValue} from '@coveo/bueno'; import {Relay} from '@coveo/relay'; import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; import { AsyncThunkCommerceOptions, isErrorResponse, } from '../../../api/commerce/commerce-api-client'; +import {ChildProduct} from '../../../api/commerce/common/product'; import {CommerceRecommendationsRequest} from '../../../api/commerce/recommendations/recommendations-request'; import {RecommendationsCommerceSuccessResponse} from '../../../api/commerce/recommendations/recommendations-response'; import {NavigatorContext} from '../../../app/navigatorContextProvider'; @@ -149,13 +150,16 @@ export const registerRecommendationsSlot = createAction( export interface PromoteChildToParentActionCreatorPayload extends SlotIdPayload { - childPermanentId: string; - parentPermanentId: string; + child: ChildProduct; } export const promoteChildToParentDefinition = { - childPermanentId: new StringValue({required: true}), - parentPermanentId: new StringValue({required: true}), + child: new RecordValue({ + options: {required: true}, + values: { + permanentid: new StringValue({required: true}), + }, + }), ...recommendationsSlotDefinition, }; diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts b/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts index 80fe1228008..ba880debbd5 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts @@ -1,4 +1,5 @@ import {Action} from '@reduxjs/toolkit'; +import {ChildProduct} from '../../../api/commerce/common/product'; import { buildMockChildProduct, buildMockProduct, @@ -306,15 +307,14 @@ describe('recommendation-slice', () => { }); describe('on #promoteChildToParent', () => { - const childPermanentId = 'child-id'; + const permanentid = 'child-id'; const parentPermanentId = 'parent-id'; let action: ReturnType; beforeEach(() => { state[slotId] = buildMockRecommendationsSlice({isLoading: false}); action = promoteChildToParent({ - childPermanentId, - parentPermanentId, + child: {permanentid} as ChildProduct, slotId, }); }); @@ -338,7 +338,7 @@ describe('recommendation-slice', () => { it('when both parent and child exist in slot, promotes the child to parent', () => { const childProduct = buildMockChildProduct({ - permanentid: childPermanentId, + permanentid, additionalFields: {test: 'test'}, clickUri: 'child-uri', ec_brand: 'child brand', diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts b/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts index e951ce7d8a3..317e0e4bf6a 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts @@ -1,6 +1,10 @@ import {createReducer} from '@reduxjs/toolkit'; import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {BaseProduct, Product} from '../../../api/commerce/common/product'; +import { + BaseProduct, + ChildProduct, + Product, +} from '../../../api/commerce/common/product'; import {RecommendationsCommerceSuccessResponse} from '../../../api/commerce/recommendations/recommendations-response'; import { fetchRecommendations, @@ -88,28 +92,23 @@ export const recommendationsReducer = createReducer( } const {products} = recommendations; - const currentParentIndex = products.findIndex( - (product) => product.permanentid === action.payload.parentPermanentId - ); - - if (currentParentIndex === -1) { + let childToPromote; + const currentParentIndex = products.findIndex((product) => { + childToPromote = product.children.find( + (child) => child.permanentid === action.payload.child.permanentid + ); + return !!childToPromote; + }); + + if (currentParentIndex === -1 || childToPromote === undefined) { return; } const position = products[currentParentIndex].position; - const {children, totalNumberOfChildren} = products[currentParentIndex]; - const childToPromote = children.find( - (child) => child.permanentid === action.payload.childPermanentId - ); - - if (childToPromote === undefined) { - return; - } - const newParent: Product = { - ...childToPromote, + ...(childToPromote as ChildProduct), children, totalNumberOfChildren, position, diff --git a/packages/headless/src/features/commerce/search/search-actions.ts b/packages/headless/src/features/commerce/search/search-actions.ts index 8772ee8283f..2cf3da8c2f1 100644 --- a/packages/headless/src/features/commerce/search/search-actions.ts +++ b/packages/headless/src/features/commerce/search/search-actions.ts @@ -1,9 +1,10 @@ -import {BooleanValue, StringValue} from '@coveo/bueno'; +import {BooleanValue, RecordValue, StringValue} from '@coveo/bueno'; import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; import { AsyncThunkCommerceOptions, isErrorResponse, } from '../../../api/commerce/commerce-api-client'; +import {ChildProduct} from '../../../api/commerce/common/product'; import {SearchCommerceSuccessResponse} from '../../../api/commerce/search/response'; import {validatePayload} from '../../../utils/validate-payload'; import { @@ -176,13 +177,16 @@ export const fetchInstantProducts = createAsyncThunk< ); export interface PromoteChildToParentActionCreatorPayload { - childPermanentId: string; - parentPermanentId: string; + child: ChildProduct; } export const promoteChildToParentDefinition = { - childPermanentId: new StringValue({required: true}), - parentPermanentId: new StringValue({required: true}), + child: new RecordValue({ + options: {required: true}, + values: { + permanentid: new StringValue({required: true}), + }, + }), }; export const promoteChildToParent = createAction( diff --git a/packages/headless/src/features/commerce/search/search-slice.test.ts b/packages/headless/src/features/commerce/search/search-slice.test.ts index 667b6008fd4..0c2be7ca96c 100644 --- a/packages/headless/src/features/commerce/search/search-slice.test.ts +++ b/packages/headless/src/features/commerce/search/search-slice.test.ts @@ -1,3 +1,4 @@ +import {ChildProduct} from '../../../api/commerce/common/product'; import {buildMockCommerceRegularFacetResponse} from '../../../test/mock-commerce-facet-response'; import {buildSearchResponse} from '../../../test/mock-commerce-search'; import { @@ -249,14 +250,13 @@ describe('search-slice', () => { }); describe('on #promoteChildToParent', () => { - const childPermanentId = 'child-id'; + const permanentid = 'child-id'; const parentPermanentId = 'parent-id'; let action: ReturnType; beforeEach(() => { action = promoteChildToParent({ - childPermanentId, - parentPermanentId, + child: {permanentid} as ChildProduct, }); }); @@ -278,7 +278,7 @@ describe('search-slice', () => { it('when both parent and child exist, promotes the child to parent', () => { const childProduct = buildMockChildProduct({ - permanentid: childPermanentId, + permanentid, additionalFields: {test: 'test'}, clickUri: 'child-uri', ec_brand: 'child brand', diff --git a/packages/headless/src/features/commerce/search/search-slice.ts b/packages/headless/src/features/commerce/search/search-slice.ts index 9c21632652e..03de594c5c8 100644 --- a/packages/headless/src/features/commerce/search/search-slice.ts +++ b/packages/headless/src/features/commerce/search/search-slice.ts @@ -1,6 +1,10 @@ import {createReducer} from '@reduxjs/toolkit'; import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; -import {Product, BaseProduct} from '../../../api/commerce/common/product'; +import { + Product, + BaseProduct, + ChildProduct, +} from '../../../api/commerce/common/product'; import {CommerceSuccessResponse} from '../../../api/commerce/common/response'; import { QuerySearchCommerceAPIThunkReturn, @@ -60,28 +64,23 @@ export const commerceSearchReducer = createReducer( }) .addCase(promoteChildToParent, (state, action) => { const {products} = state; - const currentParentIndex = products.findIndex( - (product) => product.permanentid === action.payload.parentPermanentId - ); - - if (currentParentIndex === -1) { + let childToPromote; + const currentParentIndex = products.findIndex((product) => { + childToPromote = product.children.find( + (child) => child.permanentid === action.payload.child.permanentid + ); + return !!childToPromote; + }); + + if (currentParentIndex === -1 || childToPromote === undefined) { return; } const position = products[currentParentIndex].position; - const {children, totalNumberOfChildren} = products[currentParentIndex]; - const childToPromote = children.find( - (child) => child.permanentid === action.payload.childPermanentId - ); - - if (childToPromote === undefined) { - return; - } - const newParent: Product = { - ...childToPromote, + ...(childToPromote as ChildProduct), children, totalNumberOfChildren, position,