Skip to content

Commit

Permalink
feat(Commerce Atomic): add product text component (#3902)
Browse files Browse the repository at this point in the history
This PR adds atomic-product-text and atomic-commerce-text.

https://coveord.atlassian.net/browse/KIT-3150

---------

Co-authored-by: Olivier Lamothe <[email protected]>
Co-authored-by: GitHub Actions Bot <>
Co-authored-by: Frederic Beaudoin <[email protected]>
  • Loading branch information
3 people authored May 7, 2024
1 parent 784f805 commit 01a9035
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 25 deletions.
58 changes: 58 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,16 @@ export namespace Components {
*/
"maxWithoutQuery"?: number;
}
interface AtomicCommerceText {
/**
* The count value used for plurals.
*/
"count"?: number;
/**
* The string key value.
*/
"value": string;
}
/**
* The `atomic-component-error` is used by other components to return errors. This doesn't require any configuration.
*/
Expand Down Expand Up @@ -1524,6 +1534,16 @@ export namespace Components {
*/
"getTemplate": () => Promise<ProductTemplate<DocumentFragment> | null>;
}
interface AtomicProductText {
/**
* The locale key for the text to display when the configured field has no value.
*/
"default"?: string;
/**
* The product field which the component should use. This will look in the Product object first, and then in the product.additionalFields object for the fields.
*/
"field": string;
}
/**
* The `atomic-query-error` component handles fatal errors when performing a query on the index or Search API. When the error is known, it displays a link to relevant documentation link for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
Expand Down Expand Up @@ -3078,6 +3098,12 @@ declare global {
prototype: HTMLAtomicCommerceSearchBoxRecentQueriesElement;
new (): HTMLAtomicCommerceSearchBoxRecentQueriesElement;
};
interface HTMLAtomicCommerceTextElement extends Components.AtomicCommerceText, HTMLStencilElement {
}
var HTMLAtomicCommerceTextElement: {
prototype: HTMLAtomicCommerceTextElement;
new (): HTMLAtomicCommerceTextElement;
};
/**
* The `atomic-component-error` is used by other components to return errors. This doesn't require any configuration.
*/
Expand Down Expand Up @@ -3727,6 +3753,12 @@ declare global {
prototype: HTMLAtomicProductTemplateElement;
new (): HTMLAtomicProductTemplateElement;
};
interface HTMLAtomicProductTextElement extends Components.AtomicProductText, HTMLStencilElement {
}
var HTMLAtomicProductTextElement: {
prototype: HTMLAtomicProductTextElement;
new (): HTMLAtomicProductTextElement;
};
/**
* The `atomic-query-error` component handles fatal errors when performing a query on the index or Search API. When the error is known, it displays a link to relevant documentation link for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
Expand Down Expand Up @@ -4567,6 +4599,7 @@ declare global {
"atomic-commerce-search-box": HTMLAtomicCommerceSearchBoxElement;
"atomic-commerce-search-box-query-suggestions": HTMLAtomicCommerceSearchBoxQuerySuggestionsElement;
"atomic-commerce-search-box-recent-queries": HTMLAtomicCommerceSearchBoxRecentQueriesElement;
"atomic-commerce-text": HTMLAtomicCommerceTextElement;
"atomic-component-error": HTMLAtomicComponentErrorElement;
"atomic-did-you-mean": HTMLAtomicDidYouMeanElement;
"atomic-external": HTMLAtomicExternalElement;
Expand Down Expand Up @@ -4637,6 +4670,7 @@ declare global {
"atomic-product": HTMLAtomicProductElement;
"atomic-product-link": HTMLAtomicProductLinkElement;
"atomic-product-template": HTMLAtomicProductTemplateElement;
"atomic-product-text": HTMLAtomicProductTextElement;
"atomic-query-error": HTMLAtomicQueryErrorElement;
"atomic-query-summary": HTMLAtomicQuerySummaryElement;
"atomic-quickview": HTMLAtomicQuickviewElement;
Expand Down Expand Up @@ -5069,6 +5103,16 @@ declare namespace LocalJSX {
*/
"maxWithoutQuery"?: number;
}
interface AtomicCommerceText {
/**
* The count value used for plurals.
*/
"count"?: number;
/**
* The string key value.
*/
"value": string;
}
/**
* The `atomic-component-error` is used by other components to return errors. This doesn't require any configuration.
*/
Expand Down Expand Up @@ -6115,6 +6159,16 @@ declare namespace LocalJSX {
*/
"conditions"?: ProductTemplateCondition[];
}
interface AtomicProductText {
/**
* The locale key for the text to display when the configured field has no value.
*/
"default"?: string;
/**
* The product field which the component should use. This will look in the Product object first, and then in the product.additionalFields object for the fields.
*/
"field": string;
}
/**
* The `atomic-query-error` component handles fatal errors when performing a query on the index or Search API. When the error is known, it displays a link to relevant documentation link for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
Expand Down Expand Up @@ -7395,6 +7449,7 @@ declare namespace LocalJSX {
"atomic-commerce-search-box": AtomicCommerceSearchBox;
"atomic-commerce-search-box-query-suggestions": AtomicCommerceSearchBoxQuerySuggestions;
"atomic-commerce-search-box-recent-queries": AtomicCommerceSearchBoxRecentQueries;
"atomic-commerce-text": AtomicCommerceText;
"atomic-component-error": AtomicComponentError;
"atomic-did-you-mean": AtomicDidYouMean;
"atomic-external": AtomicExternal;
Expand Down Expand Up @@ -7465,6 +7520,7 @@ declare namespace LocalJSX {
"atomic-product": AtomicProduct;
"atomic-product-link": AtomicProductLink;
"atomic-product-template": AtomicProductTemplate;
"atomic-product-text": AtomicProductText;
"atomic-query-error": AtomicQueryError;
"atomic-query-summary": AtomicQuerySummary;
"atomic-quickview": AtomicQuickview;
Expand Down Expand Up @@ -7598,6 +7654,7 @@ declare module "@stencil/core" {
* The `atomic-commerce-search-box-recent-queries` component can be added as a child of an `atomic-commerce-search-box` component, allowing for the configuration of recent query suggestions.
*/
"atomic-commerce-search-box-recent-queries": LocalJSX.AtomicCommerceSearchBoxRecentQueries & JSXBase.HTMLAttributes<HTMLAtomicCommerceSearchBoxRecentQueriesElement>;
"atomic-commerce-text": LocalJSX.AtomicCommerceText & JSXBase.HTMLAttributes<HTMLAtomicCommerceTextElement>;
/**
* The `atomic-component-error` is used by other components to return errors. This doesn't require any configuration.
*/
Expand Down Expand Up @@ -7764,6 +7821,7 @@ declare module "@stencil/core" {
"atomic-product": LocalJSX.AtomicProduct & JSXBase.HTMLAttributes<HTMLAtomicProductElement>;
"atomic-product-link": LocalJSX.AtomicProductLink & JSXBase.HTMLAttributes<HTMLAtomicProductLinkElement>;
"atomic-product-template": LocalJSX.AtomicProductTemplate & JSXBase.HTMLAttributes<HTMLAtomicProductTemplateElement>;
"atomic-product-text": LocalJSX.AtomicProductText & JSXBase.HTMLAttributes<HTMLAtomicProductTextElement>;
/**
* The `atomic-query-error` component handles fatal errors when performing a query on the index or Search API. When the error is known, it displays a link to relevant documentation link for debugging purposes. When the error is unknown, it displays a small text area with the JSON content of the error.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Component, Prop, State} from '@stencil/core';
import {
InitializableComponent,
InitializeBindings,
} from '../../../utils/initialization-utils';
import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface';

/**
* @internal
* The `atomic-commerce-text` component leverages the I18n translation module through the atomic-commerce-interface.
*/
@Component({
tag: 'atomic-commerce-text',
shadow: true,
})
export class AtomicCommerceText
implements InitializableComponent<CommerceBindings>
{
@InitializeBindings() public bindings!: CommerceBindings;

private strings = {
value: () =>
this.bindings.i18n.t(this.value, {
count: this.count,
}),
};
@State() public error!: Error;

/**
* The string key value.
*/
@Prop({reflect: true}) public value!: string;
/**
* The count value used for plurals.
*/
@Prop({reflect: true}) public count?: number;

public connectedCallback() {
if (!this.value) {
this.error = new Error('The "value" attribute must be defined.');
}
}

public render() {
return this.strings.value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-
import {
InteractiveProductContext,
ProductContext,
} from '../../product-template-components/product-template-decorators';
} from '../product-template-decorators';

/**
* @internal
Expand Down Expand Up @@ -94,11 +94,10 @@ export class AtomicProductLink
{this.hasDefaultSlot ? (
<slot />
) : (
<atomic-result-text
shouldHighlight={false}
<atomic-product-text
field="ec_name"
default="no-title"
></atomic-result-text>
></atomic-product-text>
)}
</LinkWithItemAnalytics>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Product, ProductTemplatesHelpers} from '@coveo/headless/commerce';
import {Component, h, Prop, Element} from '@stencil/core';
import {isArray} from 'lodash';
import {getFieldValueCaption} from '../../../../utils/field-utils';
import {
InitializableComponent,
InitializeBindings,
} from '../../../../utils/initialization-utils';
import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface';
import {ProductContext} from '../product-template-decorators';
import {getStringValueFromProductOrNull} from '../product-utils';

/**
* @internal
* The `atomic-product-text` component renders the value of a string product field.
*/
@Component({
tag: 'atomic-product-text',
shadow: false,
})
export class AtomicProductText
implements InitializableComponent<CommerceBindings>
{
@InitializeBindings() public bindings!: CommerceBindings;
public error!: Error;

@ProductContext() private product!: Product;

@Element() private host!: HTMLElement;

/**
* The product field which the component should use.
* This will look in the Product object first, and then in the product.additionalFields object for the fields.
*/
@Prop({reflect: true}) public field!: string;
/**
* The locale key for the text to display when the configured field has no value.
*/
@Prop({reflect: true}) public default?: string;

private possiblyWarnOnBadFieldType() {
const productValueRaw = ProductTemplatesHelpers.getProductProperty(
this.product,
this.field
);
if (isArray(productValueRaw)) {
this.bindings.engine.logger.error(
`atomic-product-text cannot be used with multi value field "${this.field}" with values "${productValueRaw}".`,
this
);
}
}

public render() {
const productValueAsString = getStringValueFromProductOrNull(
this.product,
this.field
);

if (!productValueAsString && !this.default) {
this.possiblyWarnOnBadFieldType();
this.host.remove();
return;
}

if (!productValueAsString && this.default) {
this.possiblyWarnOnBadFieldType();
return (
<atomic-commerce-text
value={getFieldValueCaption(
this.field,
productValueAsString ?? this.default,
this.bindings.i18n
)}
></atomic-commerce-text>
);
}

if (productValueAsString !== null) {
this.possiblyWarnOnBadFieldType();
return (
<atomic-commerce-text
value={getFieldValueCaption(
this.field,
productValueAsString,
this.bindings.i18n
)}
></atomic-commerce-text>
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Product, ProductTemplatesHelpers} from '@coveo/headless/commerce';

export function getStringValueFromProductOrNull(
product: Product,
field: string
) {
const value = ProductTemplatesHelpers.getProductProperty(product, field);

if (typeof value !== 'string' || value.trim() === '') {
return null;
}

return value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
<script nomodule src="/build/atomic.js"></script>
<link rel="stylesheet" href="/themes/coveo.css" />
<script type="module">
import {setupEngine, setupProductListing, renderProducts} from './engine.mjs';
import {commerceEngineConfig} from './engine.mjs';

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

await customElements.whenDefined('atomic-commerce-interface');
const commerceInterface = document.querySelector('atomic-commerce-interface');
await commerceInterface.initialize(commerceEngineConfig);

await commerceInterface.initializeWithEngine(engine);
commerceInterface.executeFirstSearch();
})();
</script>
</head>

Expand All @@ -27,6 +29,7 @@ <h1>Product listing page</h1>
<atomic-commerce-interface type="product-listing">
<atomic-commerce-product-list display="grid" density="compact" image-size="small">
</atomic-commerce-product-list>

<atomic-commerce-load-more-products></atomic-commerce-load-more-products>
<atomic-commerce-pager></atomic-commerce-pager>
</atomic-commerce-interface>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
<script nomodule src="/build/atomic.js"></script>
<link rel="stylesheet" href="/themes/coveo.css" />
<script type="module">
import {setupEngine, setupProductListing, renderProducts} from './engine.mjs';
import {commerceEngineConfig} from './engine.mjs';

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

await customElements.whenDefined('atomic-commerce-interface');
const commerceInterface = document.querySelector('atomic-commerce-interface');
await commerceInterface.initialize(commerceEngineConfig);

await commerceInterface.initializeWithEngine(engine);
commerceInterface.executeFirstSearch();
})();
</script>
</head>

Expand All @@ -37,7 +39,7 @@ <h1>Product listing page</h1>
<atomic-product-link class="font-bold"></atomic-product-link>

<atomic-field-condition class="brand text-neutral-dark" if-defined="ec_brand">
<atomic-result-text should-highlight="false" field="ec_brand"></atomic-result-text>
<atomic-product-text field="ec_brand"></atomic-product-text>
</atomic-field-condition>

<atomic-field-condition class="field" if-defined="ec_rating">
Expand All @@ -49,7 +51,7 @@ <h1>Product listing page</h1>
<atomic-format-currency currency="CAD"></atomic-format-currency>
</atomic-result-number>
</atomic-field-condition>
<atomic-result-text should-highlight="false" field="ec_description"></atomic-result-text>
<atomic-product-description></atomic-product-description>
<atomic-result-fields-list>
<atomic-field-condition must-match-ec_in_stock="true">
<atomic-result-badge label="In stock"></atomic-result-badge>
Expand Down
Loading

0 comments on commit 01a9035

Please sign in to comment.