Skip to content

Commit

Permalink
feat(atomic commerce): add interactive product controller support (#4026
Browse files Browse the repository at this point in the history
)

https://coveord.atlassian.net/browse/KIT-3149
https://coveord.atlassian.net/browse/KIT-3165

---------

Co-authored-by: Nicholas Labarre <Spuffynism@users.noreply.github.com>
Co-authored-by: GitHub Actions Bot <>
  • Loading branch information
fbeaudoincoveo and Spuffynism authored Jun 11, 2024
1 parent 35160f1 commit a07f4b9
Show file tree
Hide file tree
Showing 23 changed files with 909 additions and 277 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,24 @@ export class AtomicCommerceProductList
);
}

private logWarningIfNeeded(message?: string) {
if (message) {
this.bindings.engine.logger.warn(message);
}
}

private getInteractiveProduct(product: Product) {
const parentController =
this.bindings.interfaceElement.type === 'product-listing'
? this.productListing
: this.search;

return parentController.interactiveProduct({options: {product}});
}

private getPropsForAtomicProduct(product: Product) {
return {
// TODO: add back once interactive result is implemented for products in KIT-3149
/* interactiveResult: buildInteractiveResult(this.bindings.engine, {
options: {result},
}), */
interactiveProduct: this.getInteractiveProduct(product),
product,
renderingFunction: this.itemRenderingFunction,
loadingFlag: this.loadingFlag,
Expand All @@ -243,30 +255,32 @@ export class AtomicCommerceProductList
private renderAsGrid() {
return this.productState.products.map((product, i) => {
const propsForAtomicProduct = this.getPropsForAtomicProduct(product);
const {interactiveProduct} = propsForAtomicProduct;
return (
<DisplayGrid
item={{
...product,
clickUri: product.clickUri,
title: product.ec_name ?? 'temp',
}}
// TODO KIT-3149: add back once the interactive result is implemented
//{...propsForAtomicProduct.interactiveResult}
// TODO KIT-3149: Remove these back once the interactive result is implemented
{...propsForAtomicProduct.interactiveProduct}
setRef={(element) =>
element && this.productListCommon.setNewResultRef(element, i)
}
select={function (): void {
throw new Error('Function not implemented. TODO KIT-3149');
select={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.select();
}}
beginDelayedSelect={function (): void {
throw new Error('Function not implemented. TODO KIT-3149');
beginDelayedSelect={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.beginDelayedSelect();
}}
cancelPendingSelect={function (): void {
throw new Error('Function not implemented. TODO KIT-3149');
cancelPendingSelect={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.cancelPendingSelect();
}}
>
<atomic-product {...this} {...propsForAtomicProduct}></atomic-product>
<atomic-product {...propsForAtomicProduct}></atomic-product>
</DisplayGrid>
);
});
Expand Down Expand Up @@ -328,7 +342,6 @@ export class AtomicCommerceProductList
const propsForAtomicProduct = this.getPropsForAtomicProduct(product);
return (
<atomic-product
{...this}
{...propsForAtomicProduct}
ref={(element) =>
element && this.productListCommon.setNewResultRef(element, i)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ export class AtomicCommerceRecommendationInterface
this,
'CoveoAtomic'
);

if (!this.commonInterfaceHelper.engineIsCreated()) {
return;
}

this.contextController = buildContext(this.bindings.engine);
}

public connectedCallback() {
Expand Down Expand Up @@ -233,6 +227,10 @@ export class AtomicCommerceRecommendationInterface
};
}

private initContext() {
this.contextController = buildContext(this.bindings.engine);
}

private initAriaLive() {
if (
Array.from(this.host.children).some(
Expand All @@ -246,6 +244,7 @@ export class AtomicCommerceRecommendationInterface

private async internalInitialization(initEngine: () => void) {
await this.commonInterfaceHelper.onInitialization(initEngine);
this.initContext();
}

private addResourceBundle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,50 +302,34 @@ export class AtomicCommerceRecommendationList
);
}

private getAtomicProductProps(recommendation: Product) {
private logWarningIfNeeded(message?: string) {
if (message) {
this.bindings.engine.logger.warn(message);
}
}

private getAtomicProductProps(product: Product) {
return {
interactiveProduct: this.recommendations.interactiveProduct({
options: {
product: {
...recommendation,
name: recommendation.ec_name ?? '',
price: this.getPrice(recommendation),
productId: recommendation.permanentid,
},
position: this.currentIndex,
},
options: {product},
}),
product: recommendation,
product: product,
renderingFunction: this.itemRenderingFunction,
loadingFlag: this.loadingFlag,
key: this.itemListCommon.getResultId(
recommendation.permanentid,
product.permanentid,
this.recommendationsState.responseId,
this.density,
this.imageSize
),
content: this.productTemplateProvider.getTemplateContent(recommendation),
content: this.productTemplateProvider.getTemplateContent(product),
store: this.bindings.store,
density: this.density,
display: this.display,
imageSize: this.imageSize,
};
}

private getPrice(recommendation: Product) {
if (recommendation.ec_price === undefined) {
return 0;
}

if (recommendation.ec_promo_price === undefined) {
return recommendation.ec_price;
}

return recommendation.ec_promo_price > recommendation.ec_price
? recommendation.ec_promo_price
: recommendation.ec_price;
}

private computeListDisplayClasses() {
const displayPlaceholders = !this.bindings.store.isAppLoaded();

Expand All @@ -359,20 +343,32 @@ export class AtomicCommerceRecommendationList
}

private renderAsGrid(product: Product, i: number) {
const atomicProductProps = this.getAtomicProductProps(product);
const propsForAtomicProduct = this.getAtomicProductProps(product);
const {interactiveProduct} = propsForAtomicProduct;
return (
<DisplayGrid
item={{
...product,
clickUri: product.clickUri,
title: product.ec_name ?? '',
}}
{...atomicProductProps.interactiveProduct}
select={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.select();
}}
beginDelayedSelect={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.beginDelayedSelect();
}}
cancelPendingSelect={() => {
this.logWarningIfNeeded(interactiveProduct.warningMessage);
interactiveProduct.cancelPendingSelect();
}}
setRef={(element) =>
element && this.itemListCommon.setNewResultRef(element, i)
}
>
<atomic-product {...atomicProductProps}></atomic-product>
<atomic-product {...propsForAtomicProduct}></atomic-product>
</DisplayGrid>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export class AtomicProductLink
private linkAttributes?: Attr[];
private stopPropagation?: boolean;

private logWarningIfNeed(warning?: string) {
if (warning) {
this.bindings.engine.logger.warn(warning);
}
}

public initialize() {
this.host.dispatchEvent(
buildCustomEvent(
Expand All @@ -78,16 +84,27 @@ export class AtomicProductLink
? this.product.clickUri
: 'test';

if (!this.interactiveProduct) {
return;
}

const {warningMessage} = this.interactiveProduct;

return (
<LinkWithItemAnalytics
href={href}
onSelect={() => this.interactiveProduct.select()}
onBeginDelayedSelect={() =>
this.interactiveProduct.beginDelayedSelect()
}
onCancelPendingSelect={() =>
this.interactiveProduct.cancelPendingSelect()
}
onSelect={() => {
this.logWarningIfNeed(warningMessage);
this.interactiveProduct.select();
}}
onBeginDelayedSelect={() => {
this.logWarningIfNeed(warningMessage);
this.interactiveProduct.beginDelayedSelect();
}}
onCancelPendingSelect={() => {
this.logWarningIfNeed(warningMessage);
this.interactiveProduct.cancelPendingSelect();
}}
attributes={this.linkAttributes}
stopPropagation={this.stopPropagation}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import {
buildInstantProducts,
Product,
InstantProducts,
InteractiveProduct,
CommerceEngine,
} from '@coveo/headless/commerce';
import {Component, Element, State, h, Prop, Method} from '@stencil/core';
import {InitializableComponent} from '../../../../utils/initialization-utils';
Expand Down Expand Up @@ -34,14 +32,6 @@ export type AriaLabelGenerator = (
product: Product
) => string | undefined;

// TODO: KIT-3165 Uncomment once the `buildInteractiveInstantProduct` function is implemented in headless.
function buildInteractiveInstantProduct(
_engine: CommerceEngine<{}>,
_arg: {options: {product: Product}}
): InteractiveProduct {
return {} as InteractiveProduct;
}

/**
* The `atomic-commerce-search-box-instant-products` component can be added as a child of an `atomic-search-box` component, allowing for the configuration of instant results behavior.
*
Expand Down Expand Up @@ -141,6 +131,9 @@ export class AtomicCommerceSearchBoxInstantProducts

const elements: SearchBoxSuggestionElement[] = products.map(
(product: Product) => {
const interactiveProduct = this.instantProducts.interactiveProduct({
options: {product},
});
const partialItem = getPartialInstantItemElement(
this.bindings.i18n,
this.ariaLabelGenerator?.(this.bindings, product) || product.ec_name!,
Expand All @@ -153,12 +146,7 @@ export class AtomicCommerceSearchBoxInstantProducts
key={`instant-product-${encodeForDomAttribute(product.permanentid)}`}
part="outline"
product={product}
interactiveProduct={buildInteractiveInstantProduct(
this.bindings.engine,
{
options: {product},
}
)}
interactiveProduct={interactiveProduct}
display={this.display}
density={this.density}
imageSize={this.imageSize}
Expand Down
9 changes: 6 additions & 3 deletions packages/atomic/src/pages/examples/commerce-website/cart.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import {commerceEngine} from './engine.mjs';

(async () => {
await customElements.whenDefined('atomic-commerce-interface');
const commerceInterface = document.querySelector('atomic-commerce-recommendation-interface');
await commerceInterface.initializeWithEngine(commerceEngine);
await customElements.whenDefined('atomic-commerce-recommendation-interface');
const recommendationInterfaces = document.querySelectorAll('atomic-commerce-recommendation-interface');

for (const recommendationInterface of recommendationInterfaces) {
await recommendationInterface.initializeWithEngine(commerceEngine);
}
})();
</script>
<style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
import {commerceEngine} from './engine.mjs';

(async () => {
await customElements.whenDefined('atomic-commerce-interface');
const commerceInterface = document.querySelector('atomic-commerce-recommendation-interface');
await customElements.whenDefined('atomic-commerce-recommendation-interface');
const recommendationInterfaces = document.querySelectorAll('atomic-commerce-recommendation-interface');

await commerceInterface.initializeWithEngine(commerceEngine);
for (const recommendationInterface of recommendationInterfaces) {
await recommendationInterface.initializeWithEngine(commerceEngine);
}
})();
</script>
<style>
Expand Down
16 changes: 14 additions & 2 deletions packages/headless/src/api/commerce/common/product.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export type ChildProduct = Omit<Product, 'children' | 'totalNumberOfChildren'>;
export type ChildProduct = Omit<
BaseProduct,
'children' | 'totalNumberOfChildren'
>;

// TODO: KIT-3164 update based on https://coveord.atlassian.net/browse/DOC-14667
export interface Product {
export interface BaseProduct {
/**
* The SKU of the product.
*/
Expand Down Expand Up @@ -105,3 +108,12 @@ export interface Product {
*/
totalNumberOfChildren: number;
}

export interface Product extends BaseProduct {
/**
* The 1-based product's position across the non-paginated result set.
*
* E.g., if the product is the third one on the second page, and there are 10 products per page, its position is 13 (not 3).
*/
position: number;
}
4 changes: 2 additions & 2 deletions packages/headless/src/api/commerce/common/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
SearchAPIErrorWithStatusCode,
} from '../../search/search-api-error-response';
import {Pagination} from './pagination';
import {Product} from './product';
import {BaseProduct} from './product';
import {Sort} from './sort';

export interface BaseCommerceSuccessResponse {
responseId: string;
products: Product[];
products: BaseProduct[];
pagination: Pagination;
triggers: Trigger[];
}
Expand Down
6 changes: 5 additions & 1 deletion packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export type {
export type {LogLevel, LoggerOptions} from './app/logger';
export type {NavigatorContext} from './app/navigatorContextProvider';

export type {Product, ChildProduct} from './api/commerce/common/product';
export type {
BaseProduct,
Product,
ChildProduct,
} from './api/commerce/common/product';
export type {PlatformEnvironment} from './utils/url-utils';

// Actions
Expand Down
Loading

0 comments on commit a07f4b9

Please sign in to comment.