diff --git a/package-lock.json b/package-lock.json index 4a8f6cb5b22..87f5282430d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55327,10 +55327,10 @@ }, "packages/atomic": { "name": "@coveo/atomic", - "version": "2.74.0", + "version": "2.75.0", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "0.46.0", + "@coveo/bueno": "0.46.1", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/core": "4.19.2", @@ -55349,7 +55349,7 @@ "@axe-core/playwright": "4.9.1", "@babel/core": "7.24.9", "@coveo/atomic": "file:.", - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@coveo/release": "1.0.0", "@custom-elements-manifest/analyzer": "0.10.3", "@fullhuman/postcss-purgecss": "6.0.0", @@ -55424,7 +55424,7 @@ "node": ">=12.9.0" }, "peerDependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" } }, "packages/atomic-angular": { @@ -55439,14 +55439,14 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "2.74.0", + "@coveo/atomic": "2.75.0", "rxjs": "7.8.1" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.8", "@angular/cli": "17.3.8", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@types/jasmine": "5.1.4", "@types/node": "20.14.12", "jasmine-core": "5.2.0", @@ -55460,7 +55460,7 @@ "typescript": "5.4.5" }, "peerDependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" } }, "packages/atomic-angular/node_modules/jasmine-core": { @@ -55492,22 +55492,22 @@ "version": "2.25.5", "license": "Apache-2.0", "dependencies": { - "@coveo/atomic": "2.74.0", + "@coveo/atomic": "2.75.0", "tslib": "2.6.3" }, "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" } }, "packages/atomic-hosted-page": { "name": "@coveo/atomic-hosted-page", - "version": "0.6.1", + "version": "0.6.2", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "0.46.0", - "@coveo/headless": "2.74.0", + "@coveo/bueno": "0.46.1", + "@coveo/headless": "2.75.0", "@stencil/core": "4.19.2" }, "devDependencies": { @@ -55586,12 +55586,12 @@ }, "packages/atomic-react": { "name": "@coveo/atomic-react", - "version": "2.12.2", + "version": "2.13.0", "dependencies": { - "@coveo/atomic": "2.74.0" + "@coveo/atomic": "2.75.0" }, "devDependencies": { - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@coveo/release": "1.0.0", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-node-resolve": "^15.0.0", @@ -55608,7 +55608,7 @@ "rollup-plugin-polyfill-node": "^0.13.0" }, "peerDependencies": { - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } @@ -58059,7 +58059,7 @@ }, "packages/auth": { "name": "@coveo/auth", - "version": "1.11.21", + "version": "1.11.22", "license": "Apache-2.0", "devDependencies": { "@coveo/release": "1.0.0", @@ -58659,7 +58659,7 @@ }, "packages/bueno": { "name": "@coveo/bueno", - "version": "0.46.0", + "version": "0.46.1", "license": "Apache-2.0", "devDependencies": { "@coveo/release": "1.0.0", @@ -58717,10 +58717,10 @@ }, "packages/headless": { "name": "@coveo/headless", - "version": "2.74.0", + "version": "2.75.0", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "0.46.0", + "@coveo/bueno": "0.46.1", "@coveo/relay": "0.7.10", "@coveo/relay-event-types": "9.4.0", "@microsoft/fetch-event-source": "2.0.1", @@ -58760,10 +58760,10 @@ }, "packages/headless-react": { "name": "@coveo/headless-react", - "version": "1.0.20", + "version": "1.0.21", "license": "Apache-2.0", "dependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -59318,12 +59318,12 @@ }, "packages/quantic": { "name": "@coveo/quantic", - "version": "2.54.0", + "version": "2.54.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "0.46.0", - "@coveo/headless": "2.74.0", + "@coveo/bueno": "0.46.1", + "@coveo/headless": "2.75.0", "dompurify": "3.1.6", "marked": "12.0.2" }, @@ -60635,9 +60635,9 @@ "name": "@coveo/atomic-next-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" @@ -60916,9 +60916,9 @@ "name": "@coveo/atomic-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -61160,7 +61160,7 @@ "name": "@coveo/headless-commerce-react-samples", "version": "0.1.0", "dependencies": { - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "16.0.0", "@testing-library/user-event": "14.5.2", @@ -63627,8 +63627,8 @@ "name": "@coveo/headless-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/auth": "1.11.21", - "@coveo/headless": "2.74.0", + "@coveo/auth": "1.11.22", + "@coveo/headless": "2.75.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", @@ -67061,8 +67061,8 @@ "name": "@coveo/headless-ssr-samples-common", "version": "0.0.0", "dependencies": { - "@coveo/headless": "2.74.0", - "@coveo/headless-react": "1.0.20", + "@coveo/headless": "2.75.0", + "@coveo/headless-react": "1.0.21", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" @@ -67575,10 +67575,10 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "2.74.0", - "@coveo/atomic-hosted-page": "0.6.1", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-hosted-page": "0.6.2", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -67647,8 +67647,8 @@ "name": "@coveo/atomic-stencil-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/headless": "2.75.0", "@stencil/core": "4.19.2", "stencil-router-v2": "0.6.0" }, @@ -67708,7 +67708,7 @@ "name": "@coveo/atomic-vuejs-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "2.74.0", + "@coveo/atomic": "2.75.0", "vue": "^3.4.15" }, "devDependencies": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index 6df6357c879..164ecc1a393 100644 --- a/packages/atomic-angular/package.json +++ b/packages/atomic-angular/package.json @@ -20,17 +20,17 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "2.74.0", + "@coveo/atomic": "2.75.0", "rxjs": "7.8.1" }, "peerDependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.8", "@angular/cli": "17.3.8", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@types/jasmine": "5.1.4", "@types/node": "20.14.12", "jasmine-core": "5.2.0", diff --git a/packages/atomic-angular/projects/atomic-angular/package.json b/packages/atomic-angular/projects/atomic-angular/package.json index 73d93182a07..4dad21f5718 100644 --- a/packages/atomic-angular/projects/atomic-angular/package.json +++ b/packages/atomic-angular/projects/atomic-angular/package.json @@ -8,10 +8,10 @@ "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" }, "dependencies": { - "@coveo/atomic": "2.74.0", + "@coveo/atomic": "2.75.0", "tslib": "2.6.3" } } diff --git a/packages/atomic-hosted-page/CHANGELOG.md b/packages/atomic-hosted-page/CHANGELOG.md index cd20c7ea8c3..4a6259ea788 100644 --- a/packages/atomic-hosted-page/CHANGELOG.md +++ b/packages/atomic-hosted-page/CHANGELOG.md @@ -1,3 +1,5 @@ +## 0.6.2 (2024-07-31) + # 0.6.0 (2024-07-17) ### Bug Fixes diff --git a/packages/atomic-hosted-page/package.json b/packages/atomic-hosted-page/package.json index d8789aeee28..1b453765e67 100644 --- a/packages/atomic-hosted-page/package.json +++ b/packages/atomic-hosted-page/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic-hosted-page", "description": "Web Component used to inject a Coveo Hosted Search Page in the DOM.", - "version": "0.6.1", + "version": "0.6.2", "repository": { "type": "git", "url": "git+https://github.com/coveo/ui-kit.git", @@ -30,8 +30,8 @@ "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest" }, "dependencies": { - "@coveo/bueno": "0.46.0", - "@coveo/headless": "2.74.0", + "@coveo/bueno": "0.46.1", + "@coveo/headless": "2.75.0", "@stencil/core": "4.19.2" }, "devDependencies": { diff --git a/packages/atomic-react/CHANGELOG.md b/packages/atomic-react/CHANGELOG.md index 81388d2060e..2b3d50c9625 100644 --- a/packages/atomic-react/CHANGELOG.md +++ b/packages/atomic-react/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.13.0 (2024-07-31) + +### Features + +- **atomic-commerce:** products-per-page ([#4107](https://github.com/coveo/ui-kit/issues/4107)) ([81e31cf](https://github.com/coveo/ui-kit/commits/81e31cff63c19f19b12babd4b10aa5b2e60c19e6)) + # 2.12.0 (2024-07-09) ### Features diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index 8c1531f2105..53817ab8f8b 100644 --- a/packages/atomic-react/package.json +++ b/packages/atomic-react/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic-react", "sideEffects": false, - "version": "2.12.2", + "version": "2.13.0", "description": "React specific wrapper for the Atomic component library", "repository": { "type": "git", @@ -29,11 +29,11 @@ "commerce/" ], "dependencies": { - "@coveo/atomic": "2.74.0" + "@coveo/atomic": "2.75.0" }, "devDependencies": { "@coveo/release": "1.0.0", - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "@rollup/plugin-replace": "^5.0.0", @@ -49,7 +49,7 @@ "@rollup/plugin-terser": "0.4.4" }, "peerDependencies": { - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } diff --git a/packages/atomic/CHANGELOG.md b/packages/atomic/CHANGELOG.md index 087edf5358e..0eb4a82cd6a 100644 --- a/packages/atomic/CHANGELOG.md +++ b/packages/atomic/CHANGELOG.md @@ -1,3 +1,10 @@ +# 2.75.0 (2024-07-31) + +### Features + +- **atomic-commerce:** products-per-page ([#4107](https://github.com/coveo/ui-kit/issues/4107)) ([81e31cf](https://github.com/coveo/ui-kit/commits/81e31cff63c19f19b12babd4b10aa5b2e60c19e6)) +- **atomic:** add atomic-tab-manager component ([#4196](https://github.com/coveo/ui-kit/issues/4196)) ([523ab9b](https://github.com/coveo/ui-kit/commits/523ab9be9ba9bf8d14ef304d98726680bea63b71)) + # 2.74.0 (2024-07-24) ### Bug Fixes diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 05402a2a972..ec92c569488 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/atomic", - "version": "2.74.0", + "version": "2.75.0", "description": "A web-component library for building modern UIs interfacing with the Coveo platform", "homepage": "https://docs.coveo.com/en/atomic/latest/", "repository": { @@ -47,7 +47,7 @@ "validate:definitions": "tsc --noEmit --esModuleInterop --skipLibCheck ./dist/types/components.d.ts" }, "dependencies": { - "@coveo/bueno": "0.46.0", + "@coveo/bueno": "0.46.1", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/core": "4.19.2", @@ -66,7 +66,7 @@ "@axe-core/playwright": "4.9.1", "@babel/core": "7.24.9", "@coveo/atomic": "file:.", - "@coveo/headless": "2.74.0", + "@coveo/headless": "2.75.0", "@coveo/release": "1.0.0", "@custom-elements-manifest/analyzer": "0.10.3", "@fullhuman/postcss-purgecss": "6.0.0", @@ -138,7 +138,7 @@ "wait-on": "7.2.0" }, "peerDependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" }, "license": "Apache-2.0", "engines": { diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.new.stories.tsx b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.new.stories.tsx index 3b2faa16976..6dc93bdf290 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.new.stories.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.new.stories.tsx @@ -4,15 +4,39 @@ import { } from '@coveo/atomic/storybookUtils/commerce-interface-wrapper'; import {parameters} from '@coveo/atomic/storybookUtils/common-meta-parameters'; import {renderComponent} from '@coveo/atomic/storybookUtils/render-component'; -import {userEvent, waitFor, expect} from '@storybook/test'; +import { + CommerceEngineConfiguration, + getSampleCommerceEngineConfiguration, +} from '@coveo/headless/commerce'; import type {Meta, StoryObj as Story} from '@storybook/web-components'; import {html} from 'lit/static-html.js'; -import {within} from 'shadow-dom-testing-library'; -const {decorator, play} = wrapInCommerceInterface({skipFirstSearch: true}); +const {context, ...restOfConfiguration} = + getSampleCommerceEngineConfiguration(); + +const productListingEngineConfiguration: Partial = + { + context: { + ...context, + country: 'US', + currency: 'USD', + language: 'en', + view: { + url: context.view.url + '/browse/promotions/ui-kit-testing', + }, + }, + ...restOfConfiguration, + }; + +const {decorator, play} = wrapInCommerceInterface({ + engineConfig: productListingEngineConfiguration, + skipFirstSearch: true, + type: 'product-listing', +}); + const meta: Meta = { component: 'atomic-commerce-breadbox', - title: 'Atomic-commerce/Breadbox', + title: 'Atomic-commerce/Interface Components/atomic-commerce-breadbox', id: 'atomic-commerce-breadbox', render: renderComponent, decorators: [decorator], @@ -38,26 +62,5 @@ export const Default: Story = { play: async (context) => { await play(context); await playExecuteFirstSearch(context); - const {canvasElement, step} = context; - const canvas = within(canvasElement); - await step('Wait for the facet values to render', async () => { - await waitFor( - () => expect(canvas.getByShadowTitle('People')).toBeInTheDocument(), - { - timeout: 30e3, - } - ); - }); - await step('Select a facet value', async () => { - const facet = canvas.getByShadowTitle('People'); - await userEvent.click(facet); - await waitFor( - () => - expect( - canvas.getByShadowTitle('Object type: People') - ).toBeInTheDocument(), - {timeout: 30e3} - ); - }); }, }; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx index d4ab22627fc..09d408cec08 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx @@ -12,6 +12,9 @@ import { Breadcrumb, CategoryFacetValue, BreadcrumbValue, + Context, + ContextState, + buildContext, } from '@coveo/headless/commerce'; import {Component, h, State, Element, Prop} from '@stencil/core'; import {FocusTargetController} from '../../../utils/accessibility-utils'; @@ -30,7 +33,10 @@ import {BreadcrumbShowLess} from '../../common/breadbox/breadcrumb-show-less'; import {BreadcrumbShowMore} from '../../common/breadbox/breadcrumb-show-more'; import {Breadcrumb as BreadboxBreadcrumb} from '../../common/breadbox/breadcrumb-types'; import {formatHumanReadable} from '../../common/facets/numeric-facet/formatter'; -import {defaultNumberFormatter} from '../../common/formats/format-common'; +import { + defaultCurrencyFormatter, + defaultNumberFormatter, +} from '../../common/formats/format-common'; import {Hidden} from '../../common/hidden'; import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface'; @@ -78,6 +84,9 @@ export class AtomicCommerceBreadbox @Element() private host!: HTMLElement; + public context!: Context; + @BindStateToController('context') contextState!: ContextState; + public searchOrListing!: Search | ProductListing; @BindStateToController('breadcrumbManager') @@ -117,6 +126,8 @@ export class AtomicCommerceBreadbox this.searchOrListing = buildSearch(this.bindings.engine); } + this.context = buildContext(this.bindings.engine); + this.breadcrumbManager = this.searchOrListing.breadcrumbManager(); if (window.ResizeObserver) { @@ -237,6 +248,13 @@ export class AtomicCommerceBreadbox ); } + private getNumberFormatter(field: string) { + if (field === 'ec_price' || field === 'ec_promo_price') { + return defaultCurrencyFormatter(this.contextState.currency); + } + return defaultNumberFormatter; + } + private valueForFacetType = ( type: string, field: string, @@ -251,7 +269,7 @@ export class AtomicCommerceBreadbox i18n: this.bindings.i18n, field: field, manualRanges: [], - formatter: defaultNumberFormatter, + formatter: this.getNumberFormatter(field), }), ]; case 'dateRange': diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts new file mode 100644 index 00000000000..e30d5eee9d3 --- /dev/null +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts @@ -0,0 +1,340 @@ +/* eslint-disable @cspell/spellchecker */ +import {Locator} from '@playwright/test'; +import {test, expect} from './fixture'; + +test.describe('Default', () => { + test.beforeEach(async ({breadbox}) => { + await breadbox.load(); + }); + + test('should be A11y compliant', async ({breadbox, makeAxeBuilder}) => { + await breadbox.getFacetValue('regular').first().click(); + + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations).toEqual([]); + }); + + test.describe('when restoring the state from URL', () => { + [ + { + facetType: 'regular', + filter: '&f-cat_color=Brown', + breadcrumbLabel: 'Color:Brown', + }, + { + facetType: 'numerical range', + filter: '&nf-ec_price=15..20 ', + breadcrumbLabel: 'Price:$15.00 to $20.00', + }, + { + facetType: 'date range', + filter: '&df-date=2024/05/27@14:32:01..2025/05/27@14:32:01', + breadcrumbLabel: 'Date:2024-05-27 to 2025-05-27', + }, + { + facetType: 'category', + filter: '&cf-ec_category=Accessories', + breadcrumbLabel: 'Category:Accessories', + }, + { + facetType: 'category (nested)', + filter: '&cf-ec_category=Accessories,Surf%20Accessories', + breadcrumbLabel: 'Category:Accessories / Surf Accessories', + }, + ].forEach(({facetType, filter, breadcrumbLabel}) => { + const baseUrl = + 'http://localhost:4400/iframe.html?args=&id=atomic-commerce-breadbox--default&viewMode=story#sortCriteria=relevance'; + + test(`should show the breadcrumb for ${facetType} facet value`, async ({ + page, + breadbox, + }) => { + await page.goto(baseUrl + filter); + await page.waitForURL(baseUrl + filter); + + const breadcrumbButton = breadbox.getBreadcrumbButtons(breadcrumbLabel); + + await expect(breadcrumbButton).toHaveText(breadcrumbLabel); + }); + }); + }); + + test.describe('when a regular facet value is selected', () => { + let firstValueText: string | RegExp; + + test.beforeEach(async ({breadbox}) => { + firstValueText = (await breadbox + .getFacetValue('regular') + .locator('span') + .first() + .textContent()) as string; + await breadbox.getFacetValue('regular', firstValueText).click(); + await breadbox + .getBreadcrumbButtons(firstValueText) + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + + await expect(breadcrumbButton).toHaveText(`Color:${firstValueText}`); + }); + }); + test.describe('when a category facet value is selected', () => { + let firstValueText: string | RegExp; + test.beforeEach(async ({breadbox}) => { + firstValueText = (await breadbox + .getFacetValue('category') + .first() + .locator('span') + .first() + .textContent()) as string; + await breadbox.getFacetValue('category', firstValueText).click(); + await breadbox + .getBreadcrumbButtons(firstValueText) + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + + await expect(breadcrumbButton).toHaveText(`Category:${firstValueText}`); + }); + test.describe('when a nested category facet value is selected', () => { + let breadcrumbText: string | RegExp; + + test.beforeEach(async ({breadbox}) => { + breadcrumbText = + (await breadbox + .getFacetValue('category') + .first() + .locator('li span') + .first() + .textContent()) + + ' / ' + + (await breadbox + .getFacetValue('nestedCategory') + .first() + .locator('span') + .first() + .textContent()); + await breadbox.getFacetValue('nestedCategory').first().click(); + await breadbox + .getBreadcrumbButtons() + .first() + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons().first(); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons().first(); + + await expect(breadcrumbButton).toHaveText(`Category:${breadcrumbText}`); + }); + }); + }); + test.describe('when a numerical facet value is selected', () => { + let firstValueText: string | RegExp; + + test.beforeEach(async ({breadbox}) => { + await breadbox.getFacetValue('numerical').first().click(); + firstValueText = (await breadbox + .getFacetValue('numerical') + .locator('span') + .first() + .textContent()) as string; + await breadbox + .getBreadcrumbButtons(firstValueText) + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + + await expect(breadcrumbButton).toHaveText('Price:' + firstValueText); + }); + }); + + test.describe('when a date range facet value is selected', () => { + let firstValueText: string | RegExp; + + test.beforeEach(async ({breadbox}) => { + await breadbox.getFacetValue('dateRange').first().click(); + firstValueText = (await breadbox + .getFacetValue('dateRange') + .first() + .locator('span') + .nth(1) + .textContent()) as string; + await breadbox + .getBreadcrumbButtons(firstValueText) + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + + await expect(breadcrumbButton).toHaveText('Date:' + firstValueText); + }); + }); + + test.describe('when selecting multiple facet values', () => { + test.beforeEach(async ({breadbox}) => { + for (let i = 0; i < 6; i++) { + await breadbox.getFacetValue('regular').nth(i).click(); + await breadbox + .getBreadcrumbButtons() + .nth(i) + .waitFor({state: 'visible'}); + } + }); + + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test.describe('when clicking on the "Clear all" button', () => { + test.beforeEach(async ({breadbox}) => { + await breadbox.getClearAllButton().click(); + }); + + test('should hide the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).not.toBeVisible(); + }); + + test('should hide all breadcrumb buttons', async ({breadbox}) => { + await breadbox.getClearAllButton().waitFor({state: 'hidden'}); + + expect(await breadbox.getBreadcrumbButtons().count()).toBe(0); + }); + }); + + test('should group extra facet values in the "Show More" button based on the viewport size', async ({ + breadbox, + page, + }) => { + await expect(breadbox.getShowMorebutton()).not.toBeVisible(); + expect(await breadbox.getBreadcrumbButtons().count()).toBe(6); + + await page.setViewportSize({width: 240, height: 480}); + await breadbox.getBreadcrumbButtons().first().waitFor({state: 'hidden'}); + await expect(breadbox.getShowMorebutton()).toContainText('+ 6'); + + await page.setViewportSize({width: 1920, height: 480}); + await breadbox.getBreadcrumbButtons().first().waitFor({state: 'visible'}); + await expect(breadbox.getShowMorebutton()).not.toBeVisible(); + expect(await breadbox.getBreadcrumbButtons().count()).toBe(6); + }); + + test.describe('when clicking on a breadcrumb button', () => { + let firstButton: Locator; + let firstButtonText: string | RegExp; + + test.beforeEach(async ({breadbox, page}) => { + firstButton = breadbox.getBreadcrumbButtons().first(); + firstButtonText = (await firstButton.textContent()) as string; + await firstButton.click(); + await page.setViewportSize({width: 240, height: 480}); + }); + + test('should hide the breadcrumb button', async ({breadbox}) => { + await expect( + breadbox.getBreadcrumbButtons(firstButtonText) + ).not.toBeVisible(); + }); + + test('should uncheck the facet value', async ({breadbox}) => { + const facetValueButton = breadbox.getFacetValue( + 'regular', + firstButtonText + ); + if (await facetValueButton.isVisible()) { + await expect(facetValueButton).not.toBeChecked(); + } + }); + + test('should update the "Show More" button count', async ({breadbox}) => { + await expect(breadbox.getShowMorebutton()).toContainText('+ 5'); + }); + }); + + test.describe('when clicking on the "Show More" button', () => { + test.beforeEach(async ({breadbox, page}) => { + await page.setViewportSize({width: 640, height: 480}); + await breadbox.getShowMorebutton().click(); + }); + + test('should display all facet values', async ({breadbox}) => { + await expect(breadbox.getShowMorebutton()).not.toBeVisible(); + expect(await breadbox.getBreadcrumbButtons().count()).toBe(6); + }); + + test('should display the "Show Less" button', async ({breadbox}) => { + await expect(breadbox.getShowMorebutton()).not.toBeVisible(); + await expect(breadbox.getShowLessbutton()).toBeVisible(); + }); + + test('should show the "Show More" button when clicking on the "Show Less" button', async ({ + breadbox, + }) => { + await breadbox.getShowLessbutton().click(); + await expect(breadbox.getShowMorebutton()).toBeVisible(); + await expect(breadbox.getShowLessbutton()).not.toBeVisible(); + }); + }); + }); +}); diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/fixture.ts b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/fixture.ts new file mode 100644 index 00000000000..7133ef1e219 --- /dev/null +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/fixture.ts @@ -0,0 +1,18 @@ +import {test as base} from '@playwright/test'; +import { + AxeFixture, + makeAxeBuilder, +} from '../../../../../playwright-utils/base-fixture'; +import {AtomicCommerceBreadboxPageObject as Breadbox} from './page-object'; + +type MyFixtures = { + breadbox: Breadbox; +}; + +export const test = base.extend({ + makeAxeBuilder, + breadbox: async ({page}, use) => { + await use(new Breadbox(page)); + }, +}); +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts new file mode 100644 index 00000000000..fd26f53c28e --- /dev/null +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts @@ -0,0 +1,50 @@ +import type {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../playwright-utils/base-page-object'; + +export class AtomicCommerceBreadboxPageObject extends BasePageObject<'atomic-commerce-breadbox'> { + constructor(page: Page) { + super(page, 'atomic-commerce-breadbox'); + } + + getFacetValue( + facetType: + | 'regular' + | 'category' + | 'nestedCategory' + | 'numerical' + | 'dateRange', + value?: string | RegExp + ) { + const facetTypeLocators = { + regular: 'atomic-commerce-facet', + category: 'atomic-commerce-category-facet', + nestedCategory: 'atomic-commerce-category-facet [part="values"]', + numerical: 'atomic-commerce-numeric-facet', + dateRange: 'atomic-commerce-timeframe-facet', + }; + + const baseLocator = this.page + .locator(facetTypeLocators[facetType]) + .getByRole('listitem'); + return value ? baseLocator.filter({hasText: value}) : baseLocator; + } + + getBreadcrumbButtons(value?: string | RegExp) { + const baseLocator = this.page.getByLabel('Remove Inclusion filter on', { + exact: false, + }); + return value ? baseLocator.filter({hasText: value}) : baseLocator; + } + + getShowMorebutton() { + return this.page.getByLabel(/Show \d+ more filters/); + } + + getClearAllButton() { + return this.page.getByLabel('Clear all filters'); + } + + getShowLessbutton() { + return this.page.getByRole('button').filter({hasText: /Show less/}); + } +} diff --git a/packages/atomic/src/components/commerce/atomic-commerce-query-summary/e2e/page-object.ts b/packages/atomic/src/components/commerce/atomic-commerce-query-summary/e2e/page-object.ts index 17956c5a1d9..0ecf00be447 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-query-summary/e2e/page-object.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-query-summary/e2e/page-object.ts @@ -15,7 +15,9 @@ export class QuerySummaryPageObject extends BasePageObject<'atomic-commerce-quer } text(summaryRegex: RegExp) { - return this.page.getByText(summaryRegex); + return this.page + .locator(':not([role="status"])') + .filter({hasText: summaryRegex}); } get container() { diff --git a/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx b/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx index 038f7c81424..87ecd072466 100644 --- a/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx +++ b/packages/atomic/storybookUtils/commerce-interface-wrapper.tsx @@ -10,15 +10,17 @@ import type * as _ from '../src/components.d.ts'; export const wrapInCommerceInterface = ({ engineConfig, skipFirstSearch, + type = 'search', }: { engineConfig?: Partial; skipFirstSearch?: boolean; + type?: 'search' | 'product-listing'; }): { decorator: Decorator; play: (context: StoryContext) => Promise; } => ({ decorator: (story) => html` - + ${story()} `, diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 3406e30743f..4fa8d2b7a12 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.11.22 (2024-07-31) + ## 1.11.21 (2024-06-06) ## 1.11.20 (2024-05-15) diff --git a/packages/auth/package.json b/packages/auth/package.json index 1fcc2c72602..27bab812f9e 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/auth", - "version": "1.11.21", + "version": "1.11.22", "description": "Functions to help authenticate with the Coveo platform.", "main": "./dist/auth.js", "module": "./dist/auth.esm.js", diff --git a/packages/bueno/CHANGELOG.md b/packages/bueno/CHANGELOG.md index 685efae4f65..6504668f0b3 100644 --- a/packages/bueno/CHANGELOG.md +++ b/packages/bueno/CHANGELOG.md @@ -1,3 +1,5 @@ +## 0.46.1 (2024-07-31) + # 0.46.0 (2024-07-24) ### Features diff --git a/packages/bueno/package.json b/packages/bueno/package.json index dd525c1b704..2d01301b3fd 100644 --- a/packages/bueno/package.json +++ b/packages/bueno/package.json @@ -13,7 +13,7 @@ }, "types": "./dist/definitions/index.d.ts", "license": "Apache-2.0", - "version": "0.46.0", + "version": "0.46.1", "files": [ "dist/" ], diff --git a/packages/headless-react/CHANGELOG.md b/packages/headless-react/CHANGELOG.md index 170116e045b..b124666b55f 100644 --- a/packages/headless-react/CHANGELOG.md +++ b/packages/headless-react/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.21 (2024-07-31) + ## 1.0.9 (2024-06-06) ## 1.0.6 (2024-05-15) diff --git a/packages/headless-react/package.json b/packages/headless-react/package.json index 139162bb632..654f8529a7f 100644 --- a/packages/headless-react/package.json +++ b/packages/headless-react/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/headless-react", - "version": "1.0.20", + "version": "1.0.21", "description": "React utilities for SSR (Server Side Rendering) with headless", "homepage": "https://docs.coveo.com/en/headless/latest/", "repository": { @@ -33,7 +33,7 @@ "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest" }, "dependencies": { - "@coveo/headless": "2.74.0" + "@coveo/headless": "2.75.0" }, "devDependencies": { "@coveo/release": "1.0.0", diff --git a/packages/headless/CHANGELOG.md b/packages/headless/CHANGELOG.md index fcf41e8904b..be46ba88ed8 100644 --- a/packages/headless/CHANGELOG.md +++ b/packages/headless/CHANGELOG.md @@ -1,3 +1,15 @@ +# 2.75.0 (2024-07-31) + +### Bug Fixes + +- **deps:** update dependency @reduxjs/toolkit to v2.2.7 j:kit-282 ([#4232](https://github.com/coveo/ui-kit/issues/4232)) ([0d28438](https://github.com/coveo/ui-kit/commits/0d2843805abd8e0305b3349732554e63a3fdde64)) +- **ep:** do not send actionCause w/ breadcrumbResetAll ([#4207](https://github.com/coveo/ui-kit/issues/4207)) ([bcecc55](https://github.com/coveo/ui-kit/commits/bcecc552b433b5da3b9940bae96c725ea731df90)) +- **headless/commerce:** send clientId only when analytics are enabled ([#4217](https://github.com/coveo/ui-kit/issues/4217)) ([323cede](https://github.com/coveo/ui-kit/commits/323cedefc60292ec9193b01e500372ddde1ebcc6)) + +### Features + +- **headless SSR:** define the SSR Commerce Engine ([#4198](https://github.com/coveo/ui-kit/issues/4198)) ([c474a8d](https://github.com/coveo/ui-kit/commits/c474a8d9cb77aeb5b88a992847e5a233adb55123)) + # 2.74.0 (2024-07-24) ### Bug Fixes diff --git a/packages/headless/config/api-extractor/ssr.commerce.json b/packages/headless/config/api-extractor/ssr.commerce.json new file mode 100644 index 00000000000..d15a3d5616f --- /dev/null +++ b/packages/headless/config/api-extractor/ssr.commerce.json @@ -0,0 +1,8 @@ +{ + "extends": "./base.json", + "mainEntryPointFilePath": "/dist/definitions/ssr-commerce.index.d.ts", + "docModel": { + "enabled": true, + "apiJsonFilePath": "/temp/ssr-commerce.api.json" + } +} diff --git a/packages/headless/esbuild.mjs b/packages/headless/esbuild.mjs index 2561aa1dc70..0db3fbab006 100644 --- a/packages/headless/esbuild.mjs +++ b/packages/headless/esbuild.mjs @@ -17,6 +17,7 @@ const useCaseEntries = { 'case-assist': 'src/case-assist.index.ts', insight: 'src/insight.index.ts', ssr: 'src/ssr.index.ts', + 'ssr-commerce': 'src/ssr-commerce.index.ts', commerce: 'src/commerce.index.ts', }; @@ -36,6 +37,7 @@ function getUmdGlobalName(useCase) { 'case-assist': 'CoveoHeadlessCaseAssist', insight: 'CoveoHeadlessInsight', ssr: 'CoveoHeadlessSSR', + 'ssr-commerce': 'CoveoHeadlessCommerceSSR', commerce: 'CoveoHeadlessCommerce', }; diff --git a/packages/headless/package.json b/packages/headless/package.json index 6301785c0c3..f8e6a7f2567 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -14,7 +14,7 @@ }, "types": "./dist/definitions/index.d.ts", "license": "Apache-2.0", - "version": "2.74.0", + "version": "2.75.0", "files": [ "dist/", "recommendation/", @@ -23,6 +23,7 @@ "insight/", "case-assist/", "ssr/", + "ssr-commerce/", "commerce/" ], "scripts": { @@ -48,7 +49,7 @@ "pino-pretty": "^6.0.0 || ^10.0.0 || ^11.0.0" }, "dependencies": { - "@coveo/bueno": "0.46.0", + "@coveo/bueno": "0.46.1", "@coveo/relay": "0.7.10", "@coveo/relay-event-types": "9.4.0", "@microsoft/fetch-event-source": "2.0.1", diff --git a/packages/headless/src/api/knowledge/answer-slice.ts b/packages/headless/src/api/knowledge/answer-slice.ts index 362293b4142..d9c58fba92d 100644 --- a/packages/headless/src/api/knowledge/answer-slice.ts +++ b/packages/headless/src/api/knowledge/answer-slice.ts @@ -6,9 +6,12 @@ import { FetchBaseQueryError, retry, } from '@reduxjs/toolkit/query'; -import {ConfigurationSection} from '../../state/state-sections'; +import { + ConfigurationSection, + GeneratedAnswerSection, +} from '../../state/state-sections'; -type StateNeededByAnswerSlice = ConfigurationSection; +type StateNeededByAnswerSlice = ConfigurationSection & GeneratedAnswerSection; /** * `dynamicBaseQuery` is passed to the baseQuery of the createApi, @@ -21,6 +24,7 @@ const dynamicBaseQuery: BaseQueryFn< > = async (args, api, extraOptions) => { const state = api.getState() as StateNeededByAnswerSlice; const {accessToken, organizationId, platformUrl} = state.configuration; + const answerConfigurationId = state.generatedAnswer.answerConfigurationId; const updatedArgs = { ...(args as FetchArgs), headers: { @@ -30,7 +34,7 @@ const dynamicBaseQuery: BaseQueryFn< }; try { const data = fetchBaseQuery({ - baseUrl: `${platformUrl}/rest/organizations/${organizationId}`, + baseUrl: `${platformUrl}/rest/organizations/${organizationId}/answer/v1/configs/${answerConfigurationId}`, })(updatedArgs, api, extraOptions); return {data}; } catch (error) { diff --git a/packages/headless/src/api/knowledge/post-answer-evaluation.ts b/packages/headless/src/api/knowledge/post-answer-evaluation.ts new file mode 100644 index 00000000000..1b32e13e446 --- /dev/null +++ b/packages/headless/src/api/knowledge/post-answer-evaluation.ts @@ -0,0 +1,31 @@ +import {answerSlice} from './answer-slice'; + +export interface AnswerEvaluationPOSTParams { + question: string; + helpful: boolean; + answer: { + responseId: string; + text: string; + format: string; + }; + details: { + readable: Boolean | null; + documented: Boolean | null; + correctTopic: Boolean | null; + hallucinationFree: Boolean | null; + }; + correctAnswerUrl: string | null; + additionalNotes: string | null; +} + +export const answerEvaluation = answerSlice.injectEndpoints({ + endpoints: (builder) => ({ + post: builder.mutation({ + query: (body) => ({ + url: '/evaluations', + method: 'POST', + body, + }), + }), + }), +}); diff --git a/packages/headless/src/api/knowledge/stream-answer-api.ts b/packages/headless/src/api/knowledge/stream-answer-api.ts index 9a23fe96139..fc0197a9dc4 100644 --- a/packages/headless/src/api/knowledge/stream-answer-api.ts +++ b/packages/headless/src/api/knowledge/stream-answer-api.ts @@ -32,7 +32,8 @@ export type StateNeededByAnswerAPI = { DebugSection & GeneratedAnswerSection; -interface GeneratedAnswerStream { +export interface GeneratedAnswerStream { + answerId?: string; answerStyle?: GeneratedAnswerStyle; contentFormat?: GeneratedContentFormat; answer?: string; @@ -196,6 +197,14 @@ export const answerApi = answerSlice.injectEndpoints({ 'Accept-Encoding': '*', }, fetch, + onopen: async (res) => { + const answerId = res.headers.get('x-answer-id'); + if (answerId) { + updateCachedData((draft) => { + draft.answerId = answerId; + }); + } + }, onmessage: (event) => { updateCachedData((draft) => { updateCacheWithEvent(event, draft); diff --git a/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.test.ts b/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.test.ts index ccb912a372c..dfc4695b63a 100644 --- a/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.test.ts +++ b/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.test.ts @@ -1,14 +1,19 @@ -import {answerApi, fetchAnswer} from '../../../api/knowledge/stream-answer-api'; +import {answerEvaluation} from '../../../api/knowledge/post-answer-evaluation'; +import { + answerApi, + fetchAnswer, + StateNeededByAnswerAPI, +} from '../../../api/knowledge/stream-answer-api'; import { resetAnswer, updateAnswerConfigurationId, updateResponseFormat, } from '../../../features/generated-answer/generated-answer-actions'; -import {generatedAnswerAnalyticsClient} from '../../../features/generated-answer/generated-answer-analytics-actions'; import { - GeneratedAnswerState, - getGeneratedAnswerInitialState, -} from '../../../features/generated-answer/generated-answer-state'; + generatedAnswerAnalyticsClient, + GeneratedAnswerFeedbackV2, +} from '../../../features/generated-answer/generated-answer-analytics-actions'; +import {getGeneratedAnswerInitialState} from '../../../features/generated-answer/generated-answer-state'; import {queryReducer} from '../../../features/query/query-slice'; import { buildMockSearchEngine, @@ -33,8 +38,20 @@ jest.mock('../../../api/knowledge/stream-answer-api', () => { return { ...originalStreamAnswerApi, fetchAnswer: jest.fn(), + selectAnswer: () => ({ + data: {answer: 'This est une answer', answerId: '12345_6'}, + }), }; }); +jest.mock('../../../api/knowledge/post-answer-evaluation', () => ({ + answerEvaluation: { + endpoints: { + post: { + initiate: jest.fn(), + }, + }, + }, +})); describe('knowledge-generated-answer', () => { it('should be tested', () => { @@ -51,12 +68,12 @@ describe('knowledge-generated-answer', () => { ); const buildEngineWithGeneratedAnswer = ( - initialState: Partial = {} + initialState: Partial = {} ) => { const state = createMockState({ + ...initialState, generatedAnswer: { ...getGeneratedAnswerInitialState(), - ...initialState, }, }); return buildMockSearchEngine(state); @@ -93,9 +110,9 @@ describe('knowledge-generated-answer', () => { expect(generatedAnswer.state).toEqual({ ...engine.state.generatedAnswer, - answer: undefined, + answer: 'This est une answer', answerContentFormat: 'text/plain', - error: {message: undefined}, + error: {message: undefined, statusCode: undefined}, }); }); @@ -126,4 +143,41 @@ describe('knowledge-generated-answer', () => { generatedAnswer.reset(); expect(resetAnswer).toHaveBeenCalledTimes(1); }); + + it('dispatches a sendFeedback action', () => { + engine = buildEngineWithGeneratedAnswer({ + query: {q: 'this est une question', enableQuerySyntax: false}, + }); + const generatedAnswer = createGeneratedAnswer(); + const feedback: GeneratedAnswerFeedbackV2 = { + readable: 'unknown', + correctTopic: 'unknown', + documented: 'yes', + hallucinationFree: 'no', + helpful: false, + details: 'some details', + }; + const expectedArgs = { + additionalNotes: 'some details', + answer: { + format: 'text/plain', + responseId: '12345_6', + text: 'This est une answer', + }, + correctAnswerUrl: null, + details: { + correctTopic: null, + documented: true, + hallucinationFree: false, + readable: null, + }, + helpful: false, + question: 'this est une question', + }; + generatedAnswer.sendFeedback(feedback); + expect(answerEvaluation.endpoints.post.initiate).toHaveBeenCalledTimes(1); + expect(answerEvaluation.endpoints.post.initiate).toHaveBeenCalledWith( + expectedArgs + ); + }); }); diff --git a/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.ts b/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.ts index f996694effd..24c77370efa 100644 --- a/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.ts +++ b/packages/headless/src/controllers/knowledge/generated-answer/headless-answerapi-generated-answer.ts @@ -1,6 +1,11 @@ +import { + answerEvaluation, + AnswerEvaluationPOSTParams, +} from '../../../api/knowledge/post-answer-evaluation'; import { answerApi, fetchAnswer, + GeneratedAnswerStream, selectAnswer, selectAnswerTriggerParams, StateNeededByAnswerAPI, @@ -10,6 +15,7 @@ import { resetAnswer, updateAnswerConfigurationId, } from '../../../features/generated-answer/generated-answer-actions'; +import {GeneratedAnswerFeedbackV2} from '../../../features/generated-answer/generated-answer-analytics-actions'; import {queryReducer as query} from '../../../features/query/query-slice'; import { GeneratedAnswerSection, @@ -24,11 +30,17 @@ import { GeneratedResponseFormat, } from '../../core/generated-answer/headless-core-generated-answer'; -export interface AnswerApiGeneratedAnswer extends GeneratedAnswer { +export interface AnswerApiGeneratedAnswer + extends Omit { /** * Resets the last answer. */ reset(): void; + /** + * Sends feedback about why the generated answer was not relevant. + * @param feedback - The feedback that the end user wishes to send. + */ + sendFeedback(feedback: GeneratedAnswerFeedbackV2): void; } interface AnswerApiGeneratedAnswerProps extends GeneratedAnswerProps {} @@ -36,6 +48,46 @@ interface AnswerApiGeneratedAnswerProps extends GeneratedAnswerProps {} export interface SearchAPIGeneratedAnswerAnalyticsClient extends GeneratedAnswerAnalyticsClient {} +interface ParseEvaluationArgumentsParams { + feedback: GeneratedAnswerFeedbackV2; + answerApiState: GeneratedAnswerStream; + query: string; +} + +const parseEvaluationDetails = ( + detail: 'yes' | 'no' | 'unknown' +): Boolean | null => { + if (detail === 'yes') { + return true; + } + if (detail === 'no') { + return false; + } + return null; +}; + +const parseEvaluationArguments = ({ + answerApiState, + feedback, + query, +}: ParseEvaluationArgumentsParams): AnswerEvaluationPOSTParams => ({ + additionalNotes: feedback.details ?? null, + answer: { + text: answerApiState.answer!, + responseId: answerApiState.answerId!, + format: answerApiState.contentFormat ?? 'text/plain', + }, + correctAnswerUrl: feedback.documentUrl ?? null, + details: { + correctTopic: parseEvaluationDetails(feedback.correctTopic), + documented: parseEvaluationDetails(feedback.documented), + hallucinationFree: parseEvaluationDetails(feedback.hallucinationFree), + readable: parseEvaluationDetails(feedback.readable), + }, + helpful: feedback.helpful, + question: query, +}); + const subscribeToSearchRequest = ( engine: SearchEngine ) => { @@ -113,6 +165,14 @@ export function buildAnswerApiGeneratedAnswer( reset() { engine.dispatch(resetAnswer()); }, + async sendFeedback(feedback) { + const args = parseEvaluationArguments({ + query: getState().query.q, + feedback, + answerApiState: selectAnswer(engine.state).data!, + }); + engine.dispatch(answerEvaluation.endpoints.post.initiate(args)); + }, }; } diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts new file mode 100644 index 00000000000..78594b4ee68 --- /dev/null +++ b/packages/headless/src/ssr-commerce.index.ts @@ -0,0 +1,154 @@ +export type {Unsubscribe, Middleware} from '@reduxjs/toolkit'; +export {createAction, createAsyncThunk, createReducer} from '@reduxjs/toolkit'; +export type {Relay} from '@coveo/relay'; + +// Main App +export type {CommerceEngineOptions} from './app/commerce-engine/commerce-engine'; +export type {CommerceEngineConfiguration} from './app/commerce-engine/commerce-engine-configuration'; +export type { + SSRCommerceEngine as CommerceEngine, + CommerceEngineDefinition, + CommerceEngineDefinitionOptions, +} from './app/commerce-engine/commerce-engine.ssr'; +export {defineCommerceEngine} from './app/commerce-engine/commerce-engine.ssr'; +export {getSampleCommerceEngineConfiguration} from './app/commerce-engine/commerce-engine-configuration'; + +// export type +export type {CoreEngineNext, ExternalEngineOptions} from './app/engine'; +export type { + EngineConfiguration, + AnalyticsConfiguration, + AnalyticsRuntimeEnvironment, +} from './app/engine-configuration'; +export type { + ControllerDefinitionWithoutProps, + ControllerDefinitionWithProps, + ControllerDefinitionsMap, + InferControllerFromDefinition, + InferControllersMapFromDefinition, + InferControllerStaticStateFromController, + InferControllerStaticStateMapFromControllers, + InferControllerStaticStateMapFromDefinitions, +} from './app/ssr-engine/types/common'; +export type {Build} from './app/ssr-engine/types/build'; +export type { + EngineDefinition, + InferStaticState, + InferHydratedState, + InferBuildResult, +} from './app/ssr-engine/types/core-engine'; +export type {LoggerOptions} from './app/logger'; + +export type {LogLevel} from './app/logger'; + +// State +export type { + CommerceSearchParametersState, + CommerceProductListingParametersState, + CommerceAppState, +} from './state/commerce-app-state'; + +//#region Controllers +export type { + Controller, + Subscribable, +} from './controllers/controller/headless-controller'; + +export type {CategoryFacet} from './controllers/commerce/core/facets/category/headless-commerce-category-facet'; +export type { + DateFacet, + DateFacetState, +} from './controllers/commerce/core/facets/date/headless-commerce-date-facet'; +export type {RegularFacetValue} from './controllers/commerce/core/facets/headless-core-commerce-facet'; +export type { + NumericFacet, + NumericFacetState, +} from './controllers/commerce/core/facets/numeric/headless-commerce-numeric-facet'; +export type {RegularFacet} from './controllers/commerce/core/facets/regular/headless-commerce-regular-facet'; + +// TODO: KIT-3391 - export other SSR commerce controllers + +//#endregion + +//#region Grouped actions +export * from './features/commerce/context/context-actions-loader'; +export * from './features/commerce/search/search-actions-loader'; +export * from './features/commerce/product-listing/product-listing-actions-loader'; +export * from './features/commerce/recommendations/recommendations-actions-loader'; +export * from './features/commerce/pagination/pagination-actions-loader'; +export * from './features/commerce/product/product-actions-loaders'; +export * from './features/commerce/context/cart/cart-actions-loader'; +export * from './features/commerce/sort/sort-actions-loader'; +export * from './features/commerce/facets/core-facet/core-facet-actions-loader'; +export * from './features/commerce/facets/category-facet/category-facet-actions-loader'; +export * from './features/commerce/facets/regular-facet/regular-facet-actions-loader'; +export * from './features/commerce/facets/date-facet/date-facet-actions-loader'; +export * from './features/commerce/facets/numeric-facet/numeric-facet-actions-loader'; +export * from './features/commerce/query-set/query-set-actions-loader'; +export * from './features/commerce/query-suggest/query-suggest-actions-loader'; +export * from './features/commerce/configuration/configuration-actions-loader'; +export * from './features/commerce/query/query-actions-loader'; +export * from './features/commerce/search-parameters/search-parameters-actions-loader'; +export * from './features/commerce/product-listing-parameters/product-listing-parameters-actions-loader'; +export * from './features/commerce/triggers/triggers-actions-loader'; +export * from './features/commerce/instant-products/instant-products-actions-loader'; +export * from './features/commerce/recent-queries/recent-queries-actions-loader'; +export * from './features/commerce/standalone-search-box-set/standalone-search-box-set-actions-loader'; +export {buildResultTemplatesManager} from './features/result-templates/result-templates-manager'; +//#endregion + +// Types & Helpers +export {buildSSRSearchParameterSerializer} from './features/search-parameters/search-parameter-serializer.ssr'; +export type { + BaseProduct, + Product, + ChildProduct, +} from './api/commerce/common/product'; +export type { + SortCriterion, + SortByDate, + SortByField, + SortByNoSort, + SortByQRE, + SortByRelevancy, +} from './features/sort-criteria/criteria'; +export { + SortBy, + SortOrder, + buildDateSortCriterion, + buildCriterionExpression, + buildFieldSortCriterion, + buildNoSortCriterion, + buildQueryRankingExpressionSortCriterion, + buildRelevanceSortCriterion, +} from './features/sort-criteria/criteria'; +export {parseCriterionExpression} from './features/sort-criteria/criteria-parser'; +export type {Template} from './features/templates/templates-manager.ts'; +export type { + ProductTemplate, + ProductTemplateCondition, + ProductTemplatesManager, +} from './features/commerce/product-templates/product-templates-manager'; +export {ProductTemplatesHelpers} from './features/commerce/product-templates/product-templates-helpers'; + +export { + platformUrl, + analyticsUrl, + getOrganizationEndpoints, +} from './api/platform-client'; +export type {PlatformEnvironment} from './utils/url-utils'; + +export {buildSearchParameterSerializer} from './features/search-parameters/search-parameter-serializer'; +export type {HighlightKeyword} from './utils/highlight'; +export {VERSION} from './utils/version'; +export type { + RelativeDate, + RelativeDatePeriod, + RelativeDateUnit, +} from './api/search/date/relative-date'; +export { + deserializeRelativeDate, + validateRelativeDate, +} from './api/search/date/relative-date'; + +export * from './utils/query-expression/query-expression'; diff --git a/packages/headless/ssr-commerce/package.json b/packages/headless/ssr-commerce/package.json new file mode 100644 index 00000000000..1b997c60fb2 --- /dev/null +++ b/packages/headless/ssr-commerce/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "name": "ssr-commerce", + "description": "Server side rendering utilities for commerce", + "main": "../dist/ssr-commerce/headless.js", + "module": "../dist/ssr-commerce/headless.esm.js", + "browser": "../dist/browser/ssr-commerce/headless.esm.js", + "types": "../dist/definitions/ssr-commerce.index.d.ts", + "license": "Apache-2.0" +} diff --git a/packages/quantic/CHANGELOG.md b/packages/quantic/CHANGELOG.md index c266b41db56..8d6aeb26fe7 100644 --- a/packages/quantic/CHANGELOG.md +++ b/packages/quantic/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.54.1 (2024-07-31) + +### Bug Fixes + +- **ep:** do not send actionCause w/ breadcrumbResetAll ([#4207](https://github.com/coveo/ui-kit/issues/4207)) ([bcecc55](https://github.com/coveo/ui-kit/commits/bcecc552b433b5da3b9940bae96c725ea731df90)) + # 2.54.0 (2024-07-24) ### Bug Fixes diff --git a/packages/quantic/package.json b/packages/quantic/package.json index 94e7a04aaae..a932417a8b2 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/quantic", - "version": "2.54.0", + "version": "2.54.1", "description": "A Salesforce Lightning Web Component (LWC) library for building modern UIs interfacing with the Coveo platform", "author": "coveo.com", "homepage": "https://coveo.com", @@ -45,8 +45,8 @@ "postinstall": "node scripts/npm/setup-quantic.js" }, "dependencies": { - "@coveo/bueno": "0.46.0", - "@coveo/headless": "2.74.0", + "@coveo/bueno": "0.46.1", + "@coveo/headless": "2.75.0", "dompurify": "3.1.6", "marked": "12.0.2" }, diff --git a/packages/samples/atomic-next/package.json b/packages/samples/atomic-next/package.json index 71ff14a4c01..2bb191165e4 100644 --- a/packages/samples/atomic-next/package.json +++ b/packages/samples/atomic-next/package.json @@ -3,9 +3,9 @@ "version": "0.0.0", "private": true, "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/packages/samples/atomic-react/package.json b/packages/samples/atomic-react/package.json index 206fbeaa12f..1f8629bc3f7 100644 --- a/packages/samples/atomic-react/package.json +++ b/packages/samples/atomic-react/package.json @@ -4,9 +4,9 @@ "description": "Samples with atomic-react", "private": true, "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/packages/samples/headless-commerce-react/package.json b/packages/samples/headless-commerce-react/package.json index 7a592c3a506..f5086b93bff 100644 --- a/packages/samples/headless-commerce-react/package.json +++ b/packages/samples/headless-commerce-react/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@coveo/headless": "^2.74.0", + "@coveo/headless": "2.75.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "16.0.0", "@testing-library/user-event": "14.5.2", diff --git a/packages/samples/headless-react/package.json b/packages/samples/headless-react/package.json index 55fc24eefe9..c69e14f8c52 100644 --- a/packages/samples/headless-react/package.json +++ b/packages/samples/headless-react/package.json @@ -4,8 +4,8 @@ "version": "0.0.0", "private": true, "dependencies": { - "@coveo/auth": "1.11.21", - "@coveo/headless": "2.74.0", + "@coveo/auth": "1.11.22", + "@coveo/headless": "2.75.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", diff --git a/packages/samples/headless-ssr/package.json b/packages/samples/headless-ssr/package.json index 765b12ead40..902cc96616b 100644 --- a/packages/samples/headless-ssr/package.json +++ b/packages/samples/headless-ssr/package.json @@ -8,8 +8,8 @@ "e2e:watch": "cypress open --browser chrome --e2e" }, "dependencies": { - "@coveo/headless-react": "1.0.20", - "@coveo/headless": "2.74.0", + "@coveo/headless-react": "1.0.21", + "@coveo/headless": "2.75.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/packages/samples/iife/package.json b/packages/samples/iife/package.json index 631fc0d209b..688b15f906e 100644 --- a/packages/samples/iife/package.json +++ b/packages/samples/iife/package.json @@ -12,10 +12,10 @@ }, "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "2.74.0", - "@coveo/atomic-hosted-page": "0.6.1", - "@coveo/atomic-react": "2.12.2", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/atomic-hosted-page": "0.6.2", + "@coveo/atomic-react": "2.13.0", + "@coveo/headless": "2.75.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/packages/samples/stencil/package.json b/packages/samples/stencil/package.json index c5b79f67a9c..e4b9ac6f03b 100644 --- a/packages/samples/stencil/package.json +++ b/packages/samples/stencil/package.json @@ -8,8 +8,8 @@ "e2e:watch": "cypress open --browser chrome --e2e" }, "dependencies": { - "@coveo/atomic": "2.74.0", - "@coveo/headless": "2.74.0", + "@coveo/atomic": "2.75.0", + "@coveo/headless": "2.75.0", "@stencil/core": "4.19.2", "stencil-router-v2": "0.6.0" }, diff --git a/packages/samples/vuejs/package.json b/packages/samples/vuejs/package.json index 46d9c949fa3..d8eccef70cb 100644 --- a/packages/samples/vuejs/package.json +++ b/packages/samples/vuejs/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "vue": "^3.4.15", - "@coveo/atomic": "2.74.0" + "@coveo/atomic": "2.75.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.3",