Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(headless-ssr-commerce): fix client side recommendation refresh with a productId #4793

Merged
merged 22 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions packages/headless-react/src/ssr-commerce/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
InferHydratedState,
InferStaticState,
NavigatorContext,
ControllerWithKind,
Kind,
} from '@coveo/headless/ssr-commerce';
import {PropsWithChildren, useEffect, useState} from 'react';
import {defineCommerceEngine} from './commerce-engine.js';
Expand Down Expand Up @@ -46,19 +48,38 @@ export function buildProviderWithDefinition(looseDefinition: LooseDefinition) {
useEffect(() => {
const {searchActions, controllers} = staticState;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hydrateControllers: Record<string, any> = {};
const hydrateControllers: Record<string, object> = {};

if ('cart' in controllers) {
hydrateControllers.cart = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialState: {items: (controllers as any).cart.state.items},
};
}
for (const [key, controller] of Object.entries(controllers)) {
const typedController = controller as ControllerWithKind;

switch (typedController._kind) {
case Kind.Cart:
hydrateControllers[key] = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialState: {items: (controllers as any)[key].state.items},
};
break;

case Kind.Context:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hydrateControllers[key] = (controllers as any)[key].state;

break;

case Kind.ParameterManager:
// TODO
break;

case Kind.Recommendations:
hydrateControllers[key] = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
productId: (controllers as any)[key].state.productId,
};
break;

if ('context' in controllers) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hydrateControllers.context = (controllers as any).context.state;
default:
}
}

definition
Expand Down
8 changes: 8 additions & 0 deletions packages/headless/src/app/commerce-ssr-engine/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {InvalidControllerDefinition} from '../../utils/errors.js';
import {clone, filterObject, mapObject} from '../../utils/utils.js';
import {
ControllersMap,
ControllerWithKind,
InferControllerStaticStateMapFromControllers,
} from '../ssr-engine/types/common.js';
import {SSRCommerceEngine} from './factories/build-factory.js';
Expand All @@ -19,6 +20,12 @@ import {
SolutionType,
} from './types/common.js';

function hasKindProperty(
controller: Controller | ControllerWithKind
): controller is ControllerWithKind {
return '_kind' in controller;
}

export function createStaticState<TSearchAction extends UnknownAction>({
searchActions,
controllers,
Expand All @@ -32,6 +39,7 @@ export function createStaticState<TSearchAction extends UnknownAction>({
return {
controllers: mapObject(controllers, (controller) => ({
state: clone(controller.state),
...(hasKindProperty(controller) && {_kind: controller._kind}),
})) as InferControllerStaticStateMapFromControllers<ControllersMap>,
searchActions: searchActions.map((action) => clone(action)),
};
Expand Down
24 changes: 14 additions & 10 deletions packages/headless/src/app/commerce-ssr-engine/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
HasKeys,
HasRequiredKeys,
HasOptionalKeys,
ControllerWithKind,
} from '../../ssr-engine/types/common.js';
import {SSRCommerceEngine} from '../factories/build-factory.js';

Expand Down Expand Up @@ -60,7 +61,7 @@ export interface ControllerDefinitionWithoutProps<
}

export interface ControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> {
/**
Expand Down Expand Up @@ -88,7 +89,7 @@ export interface EngineStaticState<

export type ControllerDefinition<TController extends Controller> =
| ControllerDefinitionWithoutProps<TController>
| ControllerDefinitionWithProps<TController, unknown>;
| ControllerDefinitionWithProps<ControllerWithKind, unknown>;

export interface ControllerDefinitionsMap<TController extends Controller> {
[customName: string]: ControllerDefinition<TController>;
Expand All @@ -97,7 +98,10 @@ export interface ControllerDefinitionsMap<TController extends Controller> {
export type InferControllerPropsFromDefinition<
TController extends ControllerDefinition<Controller>,
> =
TController extends ControllerDefinitionWithProps<Controller, infer Props>
TController extends ControllerDefinitionWithProps<
ControllerWithKind,
infer Props
>
? HasKey<TController, typeof recommendationInternalOptionKey> extends never
? Props
: Props & RecommendationControllerSettings
Expand Down Expand Up @@ -458,7 +462,7 @@ export type SearchOnlyControllerDefinitionWithoutProps<
> = ControllerDefinitionWithoutProps<TController> & SearchOnlyController;

export type SearchOnlyControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> & SearchOnlyController;

Expand All @@ -472,7 +476,7 @@ export type ListingOnlyControllerDefinitionWithoutProps<
> = ControllerDefinitionWithoutProps<TController> & ListingOnlyController;

export type ListingOnlyControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> & ListingOnlyController;

Expand All @@ -482,7 +486,7 @@ export type RecommendationOnlyControllerDefinitionWithoutProps<
RecommendationOnlyController;

export type RecommendationOnlyControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> &
RecommendationOnlyController;
Expand All @@ -492,7 +496,7 @@ export type NonRecommendationControllerDefinitionWithoutProps<
> = ControllerDefinitionWithoutProps<TController> & NonRecommendationController;

export type NonRecommendationControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> &
NonRecommendationController;
Expand All @@ -502,7 +506,7 @@ export type UniversalControllerDefinitionWithoutProps<
> = ControllerDefinitionWithoutProps<TController> & UniversalController;

export type UniversalControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> & UniversalController;

Expand All @@ -511,7 +515,7 @@ export type SearchAndListingControllerDefinitionWithoutProps<
> = ControllerDefinitionWithoutProps<TController> & SearchAndListingController;

export type SearchAndListingControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TProps,
> = ControllerDefinitionWithProps<TController, TProps> &
SearchAndListingController;
Expand All @@ -530,7 +534,7 @@ export type SubControllerDefinitionWithoutProps<
: never;

export type SubControllerDefinitionWithProps<
TController extends Controller,
TController extends ControllerWithKind,
TDefinition extends ControllerDefinitionOption | undefined,
TProps,
> = TDefinition extends {listing?: true; search?: true} | undefined
Expand Down
6 changes: 6 additions & 0 deletions packages/headless/src/app/commerce-ssr-engine/types/kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum Kind {
Cart = 'CART',
Context = 'CONTEXT',
ParameterManager = 'PARAMETER_MANAGER',
Recommendations = 'RECOMMENDATIONS',
}
7 changes: 6 additions & 1 deletion packages/headless/src/app/ssr-engine/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {UnknownAction} from '@reduxjs/toolkit';
import type {Controller} from '../../../controllers/controller/headless-controller.js';
import {Kind} from '../../commerce-ssr-engine/types/kind.js';
import {CoreEngine, CoreEngineNext} from '../../engine.js';

export type HasKey<T, K extends PropertyKey> = T extends unknown
Expand Down Expand Up @@ -56,7 +57,11 @@ export interface ControllersPropsMap {
}

export interface ControllersMap {
[customName: string]: Controller;
[customName: string]: Controller | ControllerWithKind;
}

export interface ControllerWithKind extends Controller {
_kind: Kind;
}

export interface ControllerStaticState<TState> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {UniversalControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js';
import {Kind} from '../../../../app/commerce-ssr-engine/types/kind.js';
import {ControllerWithKind} from '../../../../app/ssr-engine/types/common.js';
import {Cart, buildCart, CartInitialState} from './headless-cart.js';

export type {CartState, CartItem, CartProps} from './headless-cart.js';
Expand All @@ -7,9 +9,16 @@ export type {Cart, CartInitialState};
export interface CartBuildProps {
initialState: CartInitialState;
}
interface InternalCart extends Cart, ControllerWithKind {
_kind: Kind.Cart;
state: Cart['state'];
}

export interface CartDefinition
extends UniversalControllerDefinitionWithProps<Cart, CartBuildProps> {}
extends UniversalControllerDefinitionWithProps<
InternalCart,
CartBuildProps
> {}

/**
* Defines a `Cart` controller instance.
Expand All @@ -25,7 +34,11 @@ export function defineCart(): CartDefinition {
search: true,
standalone: true,
recommendation: true,
buildWithProps: (engine, props) =>
buildCart(engine, {initialState: props.initialState}),
buildWithProps: (engine, props) => {
return {
...buildCart(engine, {initialState: props.initialState}),
_kind: Kind.Cart,
};
},
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {UniversalControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js';
import {Kind} from '../../../app/commerce-ssr-engine/types/kind.js';
import {ControllerWithKind} from '../../../app/ssr-engine/types/common.js';
import {
Context,
buildContext,
Expand All @@ -10,8 +12,16 @@ import {
export type {ContextState, Context, ContextProps} from './headless-context.js';
export type {View, UserLocation, ContextOptions};

interface InternalContext extends Context, ControllerWithKind {
_kind: Kind.Context;
state: Context['state'];
}

export interface ContextDefinition
extends UniversalControllerDefinitionWithProps<Context, ContextOptions> {}
extends UniversalControllerDefinitionWithProps<
InternalContext,
ContextOptions
> {}

/**
* Defines a `Context` controller instance.
Expand All @@ -27,6 +37,11 @@ export function defineContext(): ContextDefinition {
search: true,
standalone: true,
recommendation: true,
buildWithProps: (engine, props) => buildContext(engine, {options: props}),
buildWithProps: (engine, props) => {
return {
...buildContext(engine, {options: props}),
_kind: Kind.Context,
};
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
SolutionType,
SubControllerDefinitionWithProps,
} from '../../../../app/commerce-ssr-engine/types/common.js';
import {Kind} from '../../../../app/commerce-ssr-engine/types/kind.js';
import {CoreEngineNext} from '../../../../app/engine.js';
import {ControllerWithKind} from '../../../../app/ssr-engine/types/common.js';
import {commerceFacetSetReducer as commerceFacetSet} from '../../../../features/commerce/facets/facet-set/facet-set-slice.js';
import {manualNumericFacetReducer as manualNumericFacetSet} from '../../../../features/commerce/facets/numeric-facet/manual-numeric-facet-slice.js';
import {paginationReducer as commercePagination} from '../../../../features/commerce/pagination/pagination-slice.js';
Expand Down Expand Up @@ -33,6 +35,14 @@ export type {
CommerceSearchParameters,
};

//TODO: implement _kind in define function
interface InternalParameterManager<T extends Parameters>
extends ParameterManager<T>,
ControllerWithKind {
_kind: Kind.ParameterManager;
state: ParameterManager<T>['state'];
}

/**
* Defines a `ParameterManager` controller instance.
* @group Definers
Expand Down Expand Up @@ -63,7 +73,7 @@ export function defineParameterManager<
}
},
} as SubControllerDefinitionWithProps<
ParameterManager<MappedParameterTypes<typeof options>>,
InternalParameterManager<MappedParameterTypes<typeof options>>,
TOptions,
ParameterManagerProps<MappedParameterTypes<typeof options>>
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
recommendationInternalOptionKey,
RecommendationOnlyControllerDefinitionWithProps,
} from '../../../app/commerce-ssr-engine/types/common.js';
import {Kind} from '../../../app/commerce-ssr-engine/types/kind.js';
import {ControllerWithKind} from '../../../app/ssr-engine/types/common.js';
import {
RecommendationsOptions,
RecommendationsState,
Expand All @@ -21,9 +23,14 @@ export type RecommendationsDefinitionMeta = {
[recommendationInternalOptionKey]: {} & RecommendationsProps['options'];
};

interface InternalRecommendations extends Recommendations, ControllerWithKind {
_kind: Kind.Recommendations;
state: Recommendations['state'];
}

export interface RecommendationsDefinition
extends RecommendationOnlyControllerDefinitionWithProps<
Recommendations,
InternalRecommendations,
Partial<RecommendationsOptions>
> {}

Expand All @@ -48,9 +55,19 @@ export function defineRecommendations(
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 @@ -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
Loading
Loading