Skip to content

Commit

Permalink
fix(headless-ssr-commerce): fix client side recommendation refresh wi…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexprudhomme authored Dec 19, 2024
1 parent 7d02e19 commit 454178b
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 10 deletions.
15 changes: 12 additions & 3 deletions packages/headless-react/src/ssr-commerce/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
Context,
HydrateStaticStateOptions,
ParameterManager,
Parameters, // Recommendations,
Parameters,
Recommendations,
} from '@coveo/headless/ssr-commerce';
import {PropsWithChildren, useEffect, useState} from 'react';
import {ReactCommerceEngineDefinition} from './commerce-engine.js';
Expand Down Expand Up @@ -90,9 +91,17 @@ export function buildProviderWithDefinition<
};
break;
}
case Kind.Recommendations:
//KIT-3801: Done here
case Kind.Recommendations: {
const recommendations = getController<Recommendations>(
controllers,
key
);

hydrateArguments[key] = {
productId: recommendations.state.productId,
};
break;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
recommendationInternalOptionKey,
RecommendationOnlyControllerDefinitionWithProps,
} from '../../../app/commerce-ssr-engine/types/common.js';
import {Kind} from '../../../app/commerce-ssr-engine/types/kind.js';
import {
RecommendationsOptions,
RecommendationsState,
Expand Down Expand Up @@ -43,15 +44,24 @@ export function defineRecommendations(
[recommendationInternalOptionKey]: {
...props.options,
},
//@ts-expect-error fixed in KIT-3801
buildWithProps: (
engine,
options: Omit<RecommendationsOptions, 'slotId'>
) => {
const staticOptions = props.options;
return buildRecommendations(engine, {
const controller = buildRecommendations(engine, {
options: {...staticOptions, ...options},
});
const copy = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(controller)
);

Object.defineProperty(copy, '_kind', {
value: Kind.Recommendations,
});

return copy as typeof controller & {_kind: Kind.Recommendations};
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ChildProduct} from '../../../api/commerce/common/product.js';
import {
fetchRecommendations,
promoteChildToParent,
registerRecommendationsSlot,
} from '../../../features/commerce/recommendations/recommendations-actions.js';
import {recommendationsReducer} from '../../../features/commerce/recommendations/recommendations-slice.js';
import {buildMockCommerceState} from '../../../test/mock-commerce-state.js';
Expand All @@ -23,7 +24,7 @@ describe('headless recommendations', () => {
beforeEach(() => {
engine = buildMockCommerceEngine(buildMockCommerceState());
recommendations = buildRecommendations(engine, {
options: {slotId: 'slot-id'},
options: {slotId: 'slot-id', productId: 'product-id'},
});
});

Expand All @@ -48,4 +49,11 @@ describe('headless recommendations', () => {
recommendations.refresh();
expect(fetchRecommendations).toHaveBeenCalled();
});

it('dispatches #registerRecommendationsSlot with the correct arguments', () => {
expect(registerRecommendationsSlot).toHaveBeenCalledWith({
slotId: 'slot-id',
productId: 'product-id',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface RecommendationsState {
error: CommerceAPIErrorStatusResponse | null;
isLoading: boolean;
responseId: string;
productId?: string;
}

export interface RecommendationsOptions {
Expand Down Expand Up @@ -133,7 +134,7 @@ export function buildRecommendations(
const {dispatch} = engine;

const {slotId, productId} = props.options;
dispatch(registerRecommendationsSlot({slotId}));
dispatch(registerRecommendationsSlot({slotId, productId}));

const recommendationStateSelector = createSelector(
(state: CommerceEngineState) => state.recommendations[slotId]!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const fetchMoreRecommendations = createAsyncThunk<

export interface SlotIdPayload {
slotId: string;
productId?: string;
}

export type RegisterRecommendationsSlotPayload = SlotIdPayload;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ describe('recommendation-slice', () => {
);
expect(finalState[slotId]).toEqual(buildMockRecommendationsSlice());
});

it('when slot does not exists, sets the #productId to the one provided', () => {
const productId = 'some-product-id';
const finalState = recommendationsReducer(
state,
registerRecommendationsSlot({slotId, productId})
);
expect(finalState[slotId]!.productId).toEqual(productId);
});
});

describe('on #fetchRecommendations.fulfilled', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ export const recommendationsReducer = createReducer(
builder
.addCase(registerRecommendationsSlot, (state, action) => {
const slotId = action.payload.slotId;
const productId = action.payload.productId;

if (slotId in state) {
return;
}

state[slotId] = buildRecommendationsSlice();
if (!productId) {
state[slotId] = buildRecommendationsSlice();
return;
}

state[slotId] = buildRecommendationsSlice({productId});
})
.addCase(fetchRecommendations.rejected, (state, action) => {
handleError(state, action.meta.arg.slotId, action.payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface RecommendationsSlice {
isLoading: boolean;
responseId: string;
products: Product[];
productId?: string;
}

/**
Expand All @@ -25,4 +26,5 @@ export const getRecommendationsSliceInitialState =
isLoading: false,
responseId: '',
products: [],
productId: undefined,
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import * as externalCartAPI from '@/actions/external-cart-api';
import ContextDropdown from '@/components/context-dropdown';
import {StandaloneProvider} from '@/components/providers/providers';
import {
RecommendationProvider,
StandaloneProvider,
} from '@/components/providers/providers';
import ViewedTogether from '@/components/recommendations/viewed-together';
import StandaloneSearchBox from '@/components/standalone-search-box';
import {standaloneEngineDefinition} from '@/lib/commerce-engine';
import {
recommendationEngineDefinition,
standaloneEngineDefinition,
} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {defaultContext} from '@/utils/context';
import {headers} from 'next/headers';
Expand Down Expand Up @@ -38,6 +45,23 @@ export default async function ProductDescriptionPage({
},
});

const recsStaticState = await recommendationEngineDefinition.fetchStaticState(
{
controllers: {
viewedTogether: {enabled: true, productId: params.productId},
cart: {initialState: {items}},
context: {
language: defaultContext.language,
country: defaultContext.country,
currency: defaultContext.currency,
view: {
url: `https://sports.barca.group/products/${params.productId}`,
},
},
},
}
);

const resolvedSearchParams = await searchParams;
const price = Number(resolvedSearchParams.price) ?? NaN;
const name = resolvedSearchParams.name ?? params.productId;
Expand All @@ -54,6 +78,13 @@ export default async function ProductDescriptionPage({
{name} ({params.productId}) - ${price}
</p>
<br />

<RecommendationProvider
staticState={recsStaticState}
navigatorContext={navigatorContext.marshal}
>
<ViewedTogether />
</RecommendationProvider>
</StandaloneProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import {useViewedTogether} from '@/lib/commerce-engine';
import ProductButtonWithImage from '../product-button-with-image';

export default function ViewedTogether() {
const {state, methods} = useViewedTogether();

return (
<>
<ul>
<h3>{state.headline}</h3>
{state.products.map((product) => (
<li key={product.ec_product_id}>
<ProductButtonWithImage methods={methods} product={product} />
</li>
))}
<button onClick={() => methods?.refresh()}>Refresh</button>
</ul>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export default {
slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174',
},
}),
viewedTogether: defineRecommendations({
options: {
slotId: 'ff5d8804-d398-4dd5-b68c-6a729c66454b',
},
}),
cart: defineCart(),
searchBox: defineSearchBox(),
context: defineContext(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const {
usePagination,
usePopularBought,
usePopularViewed,
useViewedTogether,
useProductView,
useQueryTrigger,
useRecentQueriesList,
Expand Down

0 comments on commit 454178b

Please sign in to comment.