From b3d75b2360d58d81400babb87ad6255c0d3a0820 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:37:47 -0400 Subject: [PATCH 01/57] chore: lockfile update (#4580) KIT-282 --- package-lock.json | 94 +++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8761edede7d..84640416b6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2022,12 +2022,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz", + "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", "dependencies": { - "@babel/highlight": "^7.25.7", + "@babel/highlight": "^7.25.9", "picocolors": "^1.0.0" }, "engines": { @@ -2525,19 +2524,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } @@ -2582,12 +2579,11 @@ } }, "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -2597,12 +2593,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -4539,32 +4534,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", - "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4573,13 +4566,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", - "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", + "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7", + "@babel/types": "^7.25.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -4593,7 +4585,6 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, - "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4602,14 +4593,12 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -50496,6 +50485,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } From cde560e4583d1f34c5948bfcf0e3b303206a5afc Mon Sep 17 00:00:00 2001 From: "developer-experience-bot[bot]" <91079284+developer-experience-bot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:46:03 +0000 Subject: [PATCH 02/57] [Version Bump][skip ci]: ui-kit publish @coveo/atomic@3.6.2 @coveo/atomic-react@3.1.10 @coveo/atomic-angular@3.1.10 @coveo/quantic@3.2.5 **/CHANGELOG.md **/package.json CHANGELOG.md package.json package-lock.json --- package-lock.json | 32 +++++++++---------- packages/atomic-angular/package.json | 2 +- .../projects/atomic-angular/package.json | 4 +-- packages/atomic-react/CHANGELOG.md | 4 +++ packages/atomic-react/package.json | 4 +-- packages/atomic/CHANGELOG.md | 12 +++++++ packages/atomic/package.json | 2 +- packages/quantic/CHANGELOG.md | 4 +++ packages/quantic/package.json | 2 +- packages/samples/angular/package.json | 2 +- packages/samples/atomic-next/package.json | 4 +-- packages/samples/atomic-react/package.json | 4 +-- packages/samples/iife/package.json | 4 +-- packages/samples/stencil/package.json | 2 +- packages/samples/vuejs/package.json | 2 +- 15 files changed, 52 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84640416b6d..b20c3ea94b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53514,7 +53514,7 @@ }, "packages/atomic": { "name": "@coveo/atomic", - "version": "3.4.0", + "version": "3.6.2", "license": "Apache-2.0", "dependencies": { "@coveo/bueno": "1.0.1", @@ -53632,7 +53632,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "rxjs": "7.8.1" }, "devDependencies": { @@ -53684,10 +53684,10 @@ }, "packages/atomic-angular/projects/atomic-angular": { "name": "@coveo/atomic-angular", - "version": "3.1.6", + "version": "3.1.10", "license": "Apache-2.0", "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "tslib": "2.6.3" }, "engines": { @@ -53787,9 +53787,9 @@ }, "packages/atomic-react": { "name": "@coveo/atomic-react", - "version": "3.1.6", + "version": "3.1.10", "dependencies": { - "@coveo/atomic": "3.4.0" + "@coveo/atomic": "3.6.2" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -58187,7 +58187,7 @@ }, "packages/quantic": { "name": "@coveo/quantic", - "version": "3.2.1", + "version": "3.2.5", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -60126,7 +60126,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic-angular": "3.1.6", + "@coveo/atomic-angular": "3.1.10", "rxjs": "7.8.1", "tslib": "2.6.3", "zone.js": "0.14.8" @@ -60426,8 +60426,8 @@ "name": "@coveo/atomic-next-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic": "3.6.2", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.0", "next": "14.2.5", "react": "18.3.1", @@ -60491,8 +60491,8 @@ "name": "@coveo/atomic-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic": "3.6.2", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.0", "react": "18.3.1", "react-dom": "18.3.1" @@ -64501,9 +64501,9 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "@coveo/atomic-hosted-page": "1.0.7", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.0", "react": "18.3.1", "react-dom": "18.3.1" @@ -64573,7 +64573,7 @@ "name": "@coveo/atomic-stencil-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "@coveo/bueno": "1.0.1", "@coveo/headless": "3.4.0", "@stencil/core": "4.20.0", @@ -64858,7 +64858,7 @@ "name": "@coveo/atomic-vuejs-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "vue": "^3.4.15" }, "devDependencies": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index a0c6584a746..7eed33f803a 100644 --- a/packages/atomic-angular/package.json +++ b/packages/atomic-angular/package.json @@ -20,7 +20,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "rxjs": "7.8.1" }, "peerDependencies": { diff --git a/packages/atomic-angular/projects/atomic-angular/package.json b/packages/atomic-angular/projects/atomic-angular/package.json index 7ccbf6fbd09..762be0c6edd 100644 --- a/packages/atomic-angular/projects/atomic-angular/package.json +++ b/packages/atomic-angular/projects/atomic-angular/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/atomic-angular", - "version": "3.1.6", + "version": "3.1.10", "license": "Apache-2.0", "repository": { "url": "https://github.com/coveo/ui-kit" @@ -11,7 +11,7 @@ "@coveo/headless": "3.4.0" }, "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "tslib": "2.6.3" }, "engines": { diff --git a/packages/atomic-react/CHANGELOG.md b/packages/atomic-react/CHANGELOG.md index ec24eb5e323..edca716873a 100644 --- a/packages/atomic-react/CHANGELOG.md +++ b/packages/atomic-react/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.10 (2024-10-23) + +- chore(deps): bump rollup (#4525) ([874286e](https://github.com/coveo/ui-kit/commits/874286e)), closes [#4525](https://github.com/coveo/ui-kit/issues/4525) + ## 3.1.1 (2024-09-24) - docs: document headless, atomic, and atomic-react entry points (#4455) ([3853bdc](https://github.com/coveo/ui-kit/commits/3853bdc)), closes [#4455](https://github.com/coveo/ui-kit/issues/4455) diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index 5d59ed54a9a..1a588ee903b 100644 --- a/packages/atomic-react/package.json +++ b/packages/atomic-react/package.json @@ -2,7 +2,7 @@ "name": "@coveo/atomic-react", "sideEffects": false, "type": "module", - "version": "3.1.6", + "version": "3.1.10", "description": "React specific wrapper for the Atomic component library", "repository": { "type": "git", @@ -30,7 +30,7 @@ "commerce/" ], "dependencies": { - "@coveo/atomic": "3.4.0" + "@coveo/atomic": "3.6.2" }, "devDependencies": { "@coveo/release": "1.0.0", diff --git a/packages/atomic/CHANGELOG.md b/packages/atomic/CHANGELOG.md index 3d8c2882dcd..601c16fc587 100644 --- a/packages/atomic/CHANGELOG.md +++ b/packages/atomic/CHANGELOG.md @@ -1,3 +1,15 @@ +## 3.6.2 (2024-10-23) + +- chore(deps): bump rollup (#4525) ([874286e](https://github.com/coveo/ui-kit/commits/874286e)), closes [#4525](https://github.com/coveo/ui-kit/issues/4525) +- fix(atomic): broken HTML because of formatting in CRGA markdown heading (#4522) ([9e15c6c](https://github.com/coveo/ui-kit/commits/9e15c6c)), closes [#4522](https://github.com/coveo/ui-kit/issues/4522) [/github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/generated-answer/generated-content/markdown-utils.ts#L50](https://github.com//github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/generated-answer/generated-content/markdown-utils.ts/issues/L50) +- fix(atomic): delete ./loader/package.json when building atomic (#4539) ([c39f716](https://github.com/coveo/ui-kit/commits/c39f716)), closes [#4539](https://github.com/coveo/ui-kit/issues/4539) +- fix(atomic): fix layout issue on product variants with imageSize set to none (#4521) ([cbfca7f](https://github.com/coveo/ui-kit/commits/cbfca7f)), closes [#4521](https://github.com/coveo/ui-kit/issues/4521) +- fix(atomic): prevent clicks on atomic-product-image indicators from opening the product page (#4534) ([4d53962](https://github.com/coveo/ui-kit/commits/4d53962)), closes [#4534](https://github.com/coveo/ui-kit/issues/4534) +- fix(atomic): prevent touch events on atomic-product-children from opening the product page (#4533) ([7739951](https://github.com/coveo/ui-kit/commits/7739951)), closes [#4533](https://github.com/coveo/ui-kit/issues/4533) +- fix(insight): error "getAllFacets is undefined" in insight panel interface (#4474) ([836ef3a](https://github.com/coveo/ui-kit/commits/836ef3a)), closes [#4474](https://github.com/coveo/ui-kit/issues/4474) +- feat(atomic): remove imageAltField as an array option & use image alt field prior to image not found ([ee7e1d9](https://github.com/coveo/ui-kit/commits/ee7e1d9)), closes [#4511](https://github.com/coveo/ui-kit/issues/4511) +- test(atomic): fix failing atomic-color-facet test because of source change (#4543) ([c423e15](https://github.com/coveo/ui-kit/commits/c423e15)), closes [#4543](https://github.com/coveo/ui-kit/issues/4543) + ## 3.4.0 (2024-10-16) - fix(atomic): add hover effect for atomic-product clickable element in mobile/grid (#4519) ([0828b1f](https://github.com/coveo/ui-kit/commits/0828b1f)), closes [#4519](https://github.com/coveo/ui-kit/issues/4519) diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 697c058ec44..3027abaefca 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic", "type": "module", - "version": "3.4.0", + "version": "3.6.2", "description": "A web-component library for building modern UIs interfacing with the Coveo platform", "homepage": "https://docs.coveo.com/en/atomic/latest/", "repository": { diff --git a/packages/quantic/CHANGELOG.md b/packages/quantic/CHANGELOG.md index 948d126cb42..9848ac27c8d 100644 --- a/packages/quantic/CHANGELOG.md +++ b/packages/quantic/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.2.5 (2024-10-23) + +- fix(atomic): broken HTML because of formatting in CRGA markdown heading (#4522) ([9e15c6c](https://github.com/coveo/ui-kit/commits/9e15c6c)), closes [#4522](https://github.com/coveo/ui-kit/issues/4522) [/github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/generated-answer/generated-content/markdown-utils.ts#L50](https://github.com//github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/generated-answer/generated-content/markdown-utils.ts/issues/L50) + ## 3.2.1 (2024-10-16) - chore(quantic): skip consistently failing quantic tests (#4540) ([4a1b1f6](https://github.com/coveo/ui-kit/commits/4a1b1f6)), closes [#4540](https://github.com/coveo/ui-kit/issues/4540) diff --git a/packages/quantic/package.json b/packages/quantic/package.json index 82d83d510dd..3c9e47fb901 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/quantic", - "version": "3.2.1", + "version": "3.2.5", "description": "A Salesforce Lightning Web Component (LWC) library for building modern UIs interfacing with the Coveo platform", "author": "coveo.com", "homepage": "https://coveo.com", diff --git a/packages/samples/angular/package.json b/packages/samples/angular/package.json index 6eeee45f6e6..0fc577ccf5f 100644 --- a/packages/samples/angular/package.json +++ b/packages/samples/angular/package.json @@ -19,7 +19,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic-angular": "3.1.6", + "@coveo/atomic-angular": "3.1.10", "rxjs": "7.8.1", "tslib": "2.6.3", "zone.js": "0.14.8" diff --git a/packages/samples/atomic-next/package.json b/packages/samples/atomic-next/package.json index dd0d67b5336..a423584f792 100644 --- a/packages/samples/atomic-next/package.json +++ b/packages/samples/atomic-next/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic": "3.6.2", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.0", "next": "14.2.5", "react": "18.3.1", diff --git a/packages/samples/atomic-react/package.json b/packages/samples/atomic-react/package.json index 12edd1a08ff..14fb8d33c3a 100644 --- a/packages/samples/atomic-react/package.json +++ b/packages/samples/atomic-react/package.json @@ -4,8 +4,8 @@ "description": "Samples with atomic-react", "private": true, "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic": "3.6.2", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.0", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/packages/samples/iife/package.json b/packages/samples/iife/package.json index e7a493af687..4415c2cab17 100644 --- a/packages/samples/iife/package.json +++ b/packages/samples/iife/package.json @@ -12,9 +12,9 @@ }, "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "@coveo/atomic-hosted-page": "1.0.7", - "@coveo/atomic-react": "3.1.6", + "@coveo/atomic-react": "3.1.10", "@coveo/headless": "3.4.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 eb7e698fe73..839caa516a2 100644 --- a/packages/samples/stencil/package.json +++ b/packages/samples/stencil/package.json @@ -8,7 +8,7 @@ "e2e:watch": "cypress open --browser chrome --e2e" }, "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.6.2", "@coveo/bueno": "1.0.1", "@coveo/headless": "3.4.0", "@stencil/core": "4.20.0", diff --git a/packages/samples/vuejs/package.json b/packages/samples/vuejs/package.json index 2fe855e34b5..e8002a92981 100644 --- a/packages/samples/vuejs/package.json +++ b/packages/samples/vuejs/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "vue": "^3.4.15", - "@coveo/atomic": "3.4.0" + "@coveo/atomic": "3.6.2" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.3", From 1dbd9a9341ba705338f69441f3e8c25312023375 Mon Sep 17 00:00:00 2001 From: Nico Labarre Date: Wed, 23 Oct 2024 12:14:14 -0400 Subject: [PATCH 03/57] feat(commerce): add location facets (#4562) Add support for location facets. We went with the separate controller approach as we believe the location facets might evolve differently from the regular facets over time. In follow-up PRs I: - Added support for breadcrumbs and parameters: https://github.com/coveo/ui-kit/pull/4571 - Added support in the context controller to pass-in the latitude and longitude: https://github.com/coveo/ui-kit/pull/4572 - Exported the actions through an actions loader: https://github.com/coveo/ui-kit/pull/4569 --- packages/headless/src/commerce.index.ts | 7 + .../headless-core-breadcrumb-manager.ts | 13 +- ...dless-commerce-facet-generator.ssr.test.ts | 10 +- .../headless-commerce-facet-generator.ssr.ts | 17 +- .../headless-commerce-facet-generator.test.ts | 37 ++- .../headless-commerce-facet-generator.ts | 10 +- .../facets/headless-core-commerce-facet.ts | 4 + .../headless-commerce-location-facet.test.ts | 104 ++++++ .../headless-commerce-location-facet.ts | 84 +++++ .../sub-controller/headless-sub-controller.ts | 3 + .../facets/facet-set/facet-set-slice.test.ts | 307 +++++++++++++++++- .../facets/facet-set/facet-set-slice.ts | 71 +++- .../facets/facet-set/interfaces/common.ts | 3 +- .../facets/facet-set/interfaces/request.ts | 8 + .../facets/facet-set/interfaces/response.ts | 14 +- .../location-facet/location-facet-actions.ts | 29 ++ .../pagination/pagination-slice.test.ts | 12 + .../commerce/pagination/pagination-slice.ts | 6 + .../src/test/mock-commerce-facet-response.ts | 12 + .../src/test/mock-commerce-facet-value.ts | 15 + 20 files changed, 745 insertions(+), 21 deletions(-) create mode 100644 packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.test.ts create mode 100644 packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.ts create mode 100644 packages/headless/src/features/commerce/facets/location-facet/location-facet-actions.ts diff --git a/packages/headless/src/commerce.index.ts b/packages/headless/src/commerce.index.ts index a1b9ace2b2c..4bb4ee6ab96 100644 --- a/packages/headless/src/commerce.index.ts +++ b/packages/headless/src/commerce.index.ts @@ -51,6 +51,7 @@ export * from './features/commerce/sort/sort-actions-loader.js'; export * from './features/commerce/facets/core-facet/core-facet-actions-loader.js'; export * from './features/commerce/facets/category-facet/category-facet-actions-loader.js'; export * from './features/commerce/facets/regular-facet/regular-facet-actions-loader.js'; +// TODO COMHUB-247 export location facets actions loader export * from './features/commerce/facets/date-facet/date-facet-actions-loader.js'; export * from './features/commerce/facets/numeric-facet/numeric-facet-actions-loader.js'; export * from './features/commerce/query-set/query-set-actions-loader.js'; @@ -164,6 +165,10 @@ export type { RegularFacet, RegularFacetState, } from './controllers/commerce/core/facets/regular/headless-commerce-regular-facet.js'; +export type { + LocationFacet, + LocationFacetState, +} from './controllers/commerce/core/facets/location/headless-commerce-location-facet.js'; export type { NumericFacet, NumericFacetState, @@ -178,6 +183,8 @@ export type { FacetType, FacetValueRequest, RegularFacetValue, + LocationFacetValueRequest, + LocationFacetValue, NumericRangeRequest, NumericFacetValue, DateRangeRequest, diff --git a/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts b/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts index d0155a170e0..9a34e1d17d9 100644 --- a/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts +++ b/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ts @@ -21,6 +21,7 @@ import { BaseFacetValue, CategoryFacetResponse, DateFacetResponse, + LocationFacetResponse, NumericFacetResponse, RegularFacetResponse, } from '../../../../features/commerce/facets/facet-set/interfaces/response.js'; @@ -116,7 +117,8 @@ interface ActionCreators { const facetTypeWithoutExcludeAction: FacetType = 'hierarchical'; -const actions: Record = { +// TODO: COMHUB-247 add support for location facet +const actions: Record, ActionCreators> = { regular: { toggleSelectActionCreator: toggleSelectFacetValue, toggleExcludeActionCreator: toggleExcludeFacetValue, @@ -153,7 +155,10 @@ export function buildCoreBreadcrumbManager( const controller = buildController(engine); const {dispatch} = engine; - const createBreadcrumb = (facet: AnyFacetResponse) => ({ + // TODO: COMHUB-247 add support for location facet + const createBreadcrumb = ( + facet: Exclude + ) => ({ facetId: facet.facetId, facetDisplayName: facet.displayName, field: facet.field, @@ -253,7 +258,9 @@ export function buildCoreBreadcrumbManager( (facetOrder): BreadcrumbManagerState => { const breadcrumbs = facetOrder.flatMap((facetId) => { const facet = options.facetResponseSelector(engine[stateKey], facetId); - if (hasActiveValue(facet)) { + + // TODO: COMHUB-247 add support for location facet + if (hasActiveValue(facet) && facet.type !== 'location') { return [createBreadcrumb(facet)]; } return []; diff --git a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.test.ts b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.test.ts index 169cb4e7c7e..8670d887c08 100644 --- a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.test.ts +++ b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.test.ts @@ -8,6 +8,7 @@ import { buildMockCommerceDateFacetResponse, buildMockCommerceNumericFacetResponse, buildMockCommerceRegularFacetResponse, + buildMockCommerceLocationFacetResponse, } from '../../../../../test/mock-commerce-facet-response.js'; import {buildMockCommerceState} from '../../../../../test/mock-commerce-state.js'; import { @@ -55,6 +56,9 @@ describe('SSR FacetGenerator', () => { case 'numericalRange': response = buildMockCommerceNumericFacetResponse({facetId, type}); break; + case 'location': + response = buildMockCommerceLocationFacetResponse({facetId, type}); + break; case 'regular': default: response = buildMockCommerceRegularFacetResponse({facetId, type}); @@ -117,6 +121,10 @@ describe('SSR FacetGenerator', () => { facetId: 'regular-facet', type: 'regular', }, + { + facetId: 'location-facet', + type: 'location', + }, ]; state = buildMockCommerceState(); setFacetState(facetsInEngineState); @@ -131,7 +139,7 @@ describe('SSR FacetGenerator', () => { expect(facetGenerator).toBeTruthy(); }); it('#state is an array containing the state of each facet', () => { - expect(facetGenerator.state.length).toBe(4); + expect(facetGenerator.state.length).toBe(5); expect( facetGenerator.state.map((facet) => ({ facetId: facet.facetId, diff --git a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts index 6074be97668..83449e62d2c 100644 --- a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts @@ -11,6 +11,7 @@ import {stateKey} from '../../../../../app/state-key.js'; import {facetRequestSelector} from '../../../../../features/commerce/facets/facet-set/facet-set-selector.js'; import { AnyFacetResponse, + LocationFacetValue, RegularFacetValue, } from '../../../../../features/commerce/facets/facet-set/interfaces/response.js'; import {manualNumericFacetSelector} from '../../../../../features/commerce/facets/numeric-facet/manual-numeric-facet-selectors.js'; @@ -44,6 +45,11 @@ import { FacetType, getCoreFacetState, } from '../headless-core-commerce-facet.js'; +import { + getLocationFacetState, + LocationFacet, + LocationFacetState, +} from '../location/headless-commerce-location-facet.js'; import { getNumericFacetState, NumericFacet, @@ -72,6 +78,9 @@ export type { RegularFacet, RegularFacetState, RegularFacetValue, + LocationFacet, + LocationFacetState, + LocationFacetValue, }; export type FacetGeneratorState = MappedFacetStates; @@ -87,7 +96,9 @@ type MappedFacetState = { ? DateFacetState : T extends 'hierarchical' ? CategoryFacetState - : never; + : T extends 'location' + ? LocationFacetState + : never; }; export function defineFacetGenerator< @@ -235,6 +246,10 @@ export function buildFacetGenerator( createFacetState(facetResponseSelector) as RegularFacetState, specificFacetSearchStateSelector(getEngineState(), facetId) ); + case 'location': + return getLocationFacetState( + createFacetState(facetResponseSelector) as LocationFacetState + ); } }); }, diff --git a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.test.ts b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.test.ts index a22288403df..f19d6eebc7e 100644 --- a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.test.ts +++ b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.test.ts @@ -28,6 +28,7 @@ describe('CSR FacetGenerator', () => { let facetGenerator: FacetGenerator; const mockBuildNumericFacet = vi.fn(); const mockBuildRegularFacet = vi.fn(); + const mockBuildLocationFacet = vi.fn(); const mockBuildDateFacet = vi.fn(); const mockBuildCategoryFacet = vi.fn(); const mockFetchProductsActionCreator = vi.fn(); @@ -61,6 +62,7 @@ describe('CSR FacetGenerator', () => { options = { buildNumericFacet: mockBuildNumericFacet, buildRegularFacet: mockBuildRegularFacet, + buildLocationFacet: mockBuildLocationFacet, buildDateFacet: mockBuildDateFacet, buildCategoryFacet: mockBuildCategoryFacet, fetchProductsActionCreator: mockFetchProductsActionCreator, @@ -97,6 +99,14 @@ describe('CSR FacetGenerator', () => { expect(mockBuildRegularFacet).toHaveBeenCalledWith(engine, {facetId}); }); + it('when engine facet state contains a location facet, generates a location facet controller', () => { + const facetId = 'location_facet_id'; + setFacetState([{facetId, type: 'location'}]); + + expect(facetGenerator.facets.length).toEqual(1); + expect(mockBuildLocationFacet).toHaveBeenCalledWith(engine, {facetId}); + }); + it('when engine facet state contains a numeric facet, generates a numeric facet controller', () => { const facetId = 'numeric_facet_id'; setFacetState([{facetId, type: 'numericalRange'}]); @@ -127,6 +137,10 @@ describe('CSR FacetGenerator', () => { facetId: 'regular_facet_id', type: 'regular', }, + { + facetId: 'location_facet_id', + type: 'location', + }, { facetId: 'numeric_facet_id', type: 'numericalRange', @@ -142,24 +156,29 @@ describe('CSR FacetGenerator', () => { ]; setFacetState(facets); + let index = 0; mockBuildRegularFacet.mockReturnValue({ - state: {facetId: facets[0].facetId}, + state: {facetId: facets[index++].facetId}, + }); + mockBuildLocationFacet.mockReturnValue({ + state: {facetId: facets[index++].facetId}, }); mockBuildNumericFacet.mockReturnValue({ - state: {facetId: facets[1].facetId}, + state: {facetId: facets[index++].facetId}, + }); + mockBuildDateFacet.mockReturnValue({ + state: {facetId: facets[index++].facetId}, }); - mockBuildDateFacet.mockReturnValue({state: {facetId: facets[2].facetId}}); mockBuildCategoryFacet.mockReturnValue({ - state: {facetId: facets[3].facetId}, + state: {facetId: facets[index++].facetId}, }); const facetState = facetGenerator.facets; - expect(facetState.length).toEqual(4); - expect(facetState[0].state.facetId).toEqual(facets[0].facetId); - expect(facetState[1].state.facetId).toEqual(facets[1].facetId); - expect(facetState[2].state.facetId).toEqual(facets[2].facetId); - expect(facetState[3].state.facetId).toEqual(facets[3].facetId); + expect(facetState.length).toEqual(5); + expect(facetState.map((f) => f.state.facetId)).toEqual( + facets.map((f) => f.facetId) + ); }); }); diff --git a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ts b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ts index 42e30440e2f..044173d3ca0 100644 --- a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ts +++ b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ts @@ -27,6 +27,7 @@ import { CommerceFacetOptions, CoreCommerceFacet, } from '../headless-core-commerce-facet.js'; +import {LocationFacet} from '../location/headless-commerce-location-facet.js'; import {NumericFacet} from '../numeric/headless-commerce-numeric-facet.js'; import {RegularFacet} from '../regular/headless-commerce-regular-facet.js'; import {SearchableFacetOptions} from '../searchable/headless-commerce-searchable-facet.js'; @@ -47,7 +48,7 @@ export interface FacetGenerator extends Controller { /** * The facet sub-controllers created by the facet generator. - * Array of [RegularFacet](./regular-facet), [DateRangeFacet](./date-range-facet), [NumericFacet](./numeric-facet), and [CategoryFacet](./category-facet). + * Array of [RegularFacet](./regular-facet), [DateRangeFacet](./date-range-facet), [NumericFacet](./numeric-facet), [CategoryFacet](./category-facet), and [LocationFacet](./location-facet). */ facets: GeneratedFacetControllers; @@ -79,7 +80,9 @@ export type MappedGeneratedFacetController = { ? DateFacet : T extends 'hierarchical' ? CategoryFacet - : never; + : T extends 'location' + ? LocationFacet + : never; }; type CommerceFacetBuilder< @@ -108,6 +111,7 @@ export interface FacetGeneratorOptions { buildNumericFacet: CommerceFacetBuilder; buildDateFacet: CommerceFacetBuilder; buildCategoryFacet: CommerceFacetBuilder; + buildLocationFacet: CommerceFacetBuilder; fetchProductsActionCreator: FetchProductsActionCreator; } @@ -159,6 +163,8 @@ export function buildFacetGenerator( return options.buildNumericFacet(engine, {facetId}); case 'regular': return options.buildRegularFacet(engine, {facetId}); + case 'location': + return options.buildLocationFacet(engine, {facetId}); } } ); diff --git a/packages/headless/src/controllers/commerce/core/facets/headless-core-commerce-facet.ts b/packages/headless/src/controllers/commerce/core/facets/headless-core-commerce-facet.ts index 63a3a8b33a2..4fe382b45e0 100644 --- a/packages/headless/src/controllers/commerce/core/facets/headless-core-commerce-facet.ts +++ b/packages/headless/src/controllers/commerce/core/facets/headless-core-commerce-facet.ts @@ -11,12 +11,14 @@ import {FacetType} from '../../../../features/commerce/facets/facet-set/interfac import { AnyFacetRequest, CategoryFacetValueRequest, + LocationFacetValueRequest, } from '../../../../features/commerce/facets/facet-set/interfaces/request.js'; import { AnyFacetResponse, AnyFacetValueResponse, CategoryFacetValue, DateFacetValue, + LocationFacetValue, NumericFacetValue, RegularFacetValue, } from '../../../../features/commerce/facets/facet-set/interfaces/response.js'; @@ -37,6 +39,8 @@ export type { FacetType, FacetValueRequest, RegularFacetValue, + LocationFacetValueRequest, + LocationFacetValue, NumericRangeRequest, NumericFacetValue, DateRangeRequest, diff --git a/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.test.ts b/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.test.ts new file mode 100644 index 00000000000..9878ce4fca7 --- /dev/null +++ b/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.test.ts @@ -0,0 +1,104 @@ +import {LocationFacetRequest} from '../../../../../features/commerce/facets/facet-set/interfaces/request.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../../../../../features/commerce/facets/location-facet/location-facet-actions.js'; +import {CommerceAppState} from '../../../../../state/commerce-app-state.js'; +import {buildMockCommerceFacetRequest} from '../../../../../test/mock-commerce-facet-request.js'; +import {buildMockCommerceLocationFacetResponse} from '../../../../../test/mock-commerce-facet-response.js'; +import {buildMockCommerceFacetSlice} from '../../../../../test/mock-commerce-facet-slice.js'; +import {buildMockCommerceLocationFacetValue} from '../../../../../test/mock-commerce-facet-value.js'; +import {buildMockCommerceState} from '../../../../../test/mock-commerce-state.js'; +import { + MockedCommerceEngine, + buildMockCommerceEngine, +} from '../../../../../test/mock-engine-v2.js'; +import { + LocationFacet, + LocationFacetOptions, + buildCommerceLocationFacet, +} from './headless-commerce-location-facet.js'; + +vi.mock( + '../../../../../features/commerce/facets/location-facet/location-facet-actions' +); + +describe('LocationFacet', () => { + const facetId: string = 'location_facet_id'; + let engine: MockedCommerceEngine; + let state: CommerceAppState; + let options: LocationFacetOptions; + let facet: LocationFacet; + const facetResponseSelector = vi.fn(); + + function initEngine(preloadedState = buildMockCommerceState()) { + engine = buildMockCommerceEngine(preloadedState); + } + + function initFacet() { + facet = buildCommerceLocationFacet(engine, options); + } + + function setFacetRequest(config: Partial = {}) { + state.commerceFacetSet[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({facetId, ...config}), + }); + state.productListing.facets = [ + buildMockCommerceLocationFacetResponse({facetId}), + ]; + facetResponseSelector.mockReturnValue( + buildMockCommerceLocationFacetResponse({facetId}) + ); + } + + beforeEach(() => { + vi.resetAllMocks(); + + options = { + facetId, + fetchProductsActionCreator: vi.fn(), + facetResponseSelector, + isFacetLoadingResponseSelector: vi.fn(), + }; + + state = buildMockCommerceState(); + setFacetRequest(); + + initEngine(state); + initFacet(); + }); + + describe('initialization', () => { + it('initializes', () => { + expect(facet).toBeTruthy(); + }); + + it('exposes #subscribe method', () => { + expect(facet.subscribe).toBeTruthy(); + }); + }); + + it('#toggleSelect dispatches #toggleSelectLocationFacetValue with correct payload', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + facet.toggleSelect(facetValue); + + expect(toggleSelectLocationFacetValue).toHaveBeenCalledWith({ + facetId, + selection: facetValue, + }); + }); + + it('#toggleExclude dispatches #toggleExcludeLocationFacetValue with correct payload', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + facet.toggleExclude(facetValue); + + expect(toggleExcludeLocationFacetValue).toHaveBeenCalledWith({ + facetId, + selection: facetValue, + }); + }); + + it('#type returns "location"', () => { + expect(facet.type).toBe('location'); + }); +}); diff --git a/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.ts b/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.ts new file mode 100644 index 00000000000..05d6a2e99e0 --- /dev/null +++ b/packages/headless/src/controllers/commerce/core/facets/location/headless-commerce-location-facet.ts @@ -0,0 +1,84 @@ +import {CommerceEngine} from '../../../../../app/commerce-engine/commerce-engine.js'; +import {LocationFacetValue} from '../../../../../features/commerce/facets/facet-set/interfaces/response.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../../../../../features/commerce/facets/location-facet/location-facet-actions.js'; +import { + CoreCommerceFacet, + CoreCommerceFacetOptions, + CoreCommerceFacetState, + FacetControllerType, + FacetValueRequest, + buildCoreCommerceFacet, +} from '../headless-core-commerce-facet.js'; + +export type LocationFacetOptions = Omit< + CoreCommerceFacetOptions, + 'toggleSelectActionCreator' | 'toggleExcludeActionCreator' +>; + +export type LocationFacetState = Omit< + CoreCommerceFacetState, + 'type' +> & { + type: 'location'; +}; + +/** + * The `LocationFacet` sub-controller offers a high-level programming interface for implementing a location commerce + * facet UI component. + */ +export type LocationFacet = CoreCommerceFacet< + FacetValueRequest, + LocationFacetValue +> & { + state: LocationFacetState; +} & FacetControllerType<'location'>; + +/** + * @internal + * + * **Important:** This initializer is meant for internal use by headless only. + * As an implementer, you must not import or use this initializer directly in your code. + * You will instead interact with `LocationFacet` sub-controller instances through the state of a `FacetGenerator` + * sub-controller. + * + * @param engine - The headless commerce engine. + * @param options - The `LocationFacet` options used internally. + * @returns A `LocationFacet` sub-controller instance. + * */ +export function buildCommerceLocationFacet( + engine: CommerceEngine, + options: LocationFacetOptions +): LocationFacet { + const coreController = buildCoreCommerceFacet< + FacetValueRequest, + LocationFacetValue + >(engine, { + options: { + ...options, + toggleSelectActionCreator: toggleSelectLocationFacetValue, + toggleExcludeActionCreator: toggleExcludeLocationFacetValue, + }, + }); + + return { + ...coreController, + + get state() { + return getLocationFacetState(coreController.state); + }, + + type: 'location', + }; +} + +export const getLocationFacetState = ( + coreState: CoreCommerceFacetState +): LocationFacetState => { + return { + ...coreState, + type: 'location', + }; +}; diff --git a/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ts b/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ts index b84ea80e63e..8c35a08bc6a 100644 --- a/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ts +++ b/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ts @@ -29,6 +29,7 @@ import { buildFacetGenerator, FacetGenerator, } from '../facets/generator/headless-commerce-facet-generator.js'; +import {buildCommerceLocationFacet} from '../facets/location/headless-commerce-location-facet.js'; import {buildCommerceNumericFacet} from '../facets/numeric/headless-commerce-numeric-facet.js'; import {buildCommerceRegularFacet} from '../facets/regular/headless-commerce-regular-facet.js'; import { @@ -277,6 +278,8 @@ export function buildSearchAndListingsSubControllers< buildCommerceDateFacet(engine, {...options, ...commonOptions}), buildCategoryFacet: (_engine, options) => buildCategoryFacet(engine, {...options, ...commonOptions}), + buildLocationFacet: (_engine, options) => + buildCommerceLocationFacet(engine, {...options, ...commonOptions}), fetchProductsActionCreator, }); }, diff --git a/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.test.ts b/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.test.ts index 0beae165eb2..3e24102dd3a 100644 --- a/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.test.ts +++ b/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.test.ts @@ -9,6 +9,7 @@ import {buildMockCommerceFacetRequest} from '../../../../test/mock-commerce-face import { buildMockCategoryFacetResponse, buildMockCommerceDateFacetResponse, + buildMockCommerceLocationFacetResponse, buildMockCommerceNumericFacetResponse, buildMockCommerceRegularFacetResponse, } from '../../../../test/mock-commerce-facet-response.js'; @@ -16,6 +17,7 @@ import {buildMockCommerceFacetSlice} from '../../../../test/mock-commerce-facet- import { buildMockCategoryFacetValue, buildMockCommerceDateFacetValue, + buildMockCommerceLocationFacetValue, buildMockCommerceNumericFacetValue, buildMockCommerceRegularFacetValue, } from '../../../../test/mock-commerce-facet-value.js'; @@ -65,6 +67,10 @@ import { updateDateFacetValues, } from '../date-facet/date-facet-actions.js'; import {getFacetIdWithCommerceFieldSuggestionNamespace} from '../facet-search-set/commerce-facet-search-actions.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../location-facet/location-facet-actions.js'; import { toggleExcludeNumericFacetValue, toggleSelectNumericFacetValue, @@ -78,13 +84,17 @@ import * as CommerceFacetReducers from './facet-set-reducer-helpers.js'; import { commerceFacetSetReducer, convertCategoryFacetValueToRequest, + convertLocationFacetValueToRequest, } from './facet-set-slice.js'; import { CommerceFacetSetState, getCommerceFacetSetInitialState, } from './facet-set-state.js'; import {FacetType} from './interfaces/common.js'; -import {CategoryFacetValueRequest} from './interfaces/request.js'; +import { + CategoryFacetValueRequest, + LocationFacetValueRequest, +} from './interfaces/request.js'; import {AnyFacetResponse, CategoryFacetValue} from './interfaces/response.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -557,6 +567,10 @@ describe('commerceFacetSetReducer', () => { type: 'regular' as FacetType, facetResponseBuilder: buildMockCommerceRegularFacetResponse, }, + { + type: 'location' as FacetType, + facetResponseBuilder: buildMockCommerceLocationFacetResponse, + }, { type: 'numericalRange' as FacetType, facetResponseBuilder: buildMockCommerceNumericFacetResponse, @@ -983,6 +997,296 @@ describe('commerceFacetSetReducer', () => { }); }); + describe('for location facets', () => { + describe.each([ + { + title: + 'dispatching #toggleSelectLocationFacetValue with a registered facet id', + facetValueState: 'selected' as FacetValueState, + toggleAction: toggleSelectLocationFacetValue, + }, + { + title: + 'dispatching #toggleExcludeLocationFacetValue with a registered facet id', + facetValueState: 'excluded' as FacetValueState, + toggleAction: toggleExcludeLocationFacetValue, + }, + ])( + '$title', + ({ + facetValueState, + toggleAction, + }: { + facetValueState: FacetValueState; + toggleAction: Function; + }) => { + const facetId = '1'; + const oppositeFacetValueState = facetValueStates.find( + (valueState) => ![facetValueState, 'idle'].includes(valueState) + ); + describe('when the facet value exists', () => { + it(`sets the state of an idle value to ${facetValueState}`, () => { + const facetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + }); + const facetValueRequest = + convertLocationFacetValueToRequest(facetValue); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + values: [facetValueRequest], + type: 'location', + }), + }); + + const action = toggleAction({ + facetId, + selection: facetValue, + }); + const finalState = commerceFacetSetReducer(state, action); + + const targetValue = ( + finalState[facetId]?.request.values as LocationFacetValueRequest[] + ).find((req) => req.value === facetValue.value); + expect(targetValue?.state).toBe(facetValueState); + }); + + it(`sets the state of an ${oppositeFacetValueState} value to ${facetValueState}`, () => { + const facetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + state: oppositeFacetValueState, + }); + const facetValueRequest = + convertLocationFacetValueToRequest(facetValue); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + values: [facetValueRequest], + type: 'location', + }), + }); + + const action = toggleAction({ + facetId, + selection: facetValue, + }); + const finalState = commerceFacetSetReducer(state, action); + + const targetValue = ( + finalState[facetId]?.request.values as LocationFacetValueRequest[] + ).find((req) => req.value === facetValue.value); + expect(targetValue?.state).toBe(facetValueState); + }); + + it(`sets the state of a ${facetValueState} value to idle`, () => { + const facetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + state: facetValueState, + }); + const facetValueRequest = + convertLocationFacetValueToRequest(facetValue); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + values: [facetValueRequest], + type: 'location', + }), + }); + + const action = toggleAction({ + facetId, + selection: facetValue, + }); + const finalState = commerceFacetSetReducer(state, action); + + const targetValue = ( + finalState[facetId]?.request.values as LocationFacetValueRequest[] + ).find((req) => req.value === facetValue.value); + expect(targetValue?.state).toBe('idle'); + }); + + it('sets #preventAutoSelect to true', () => { + const facetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + }); + const facetValueRequest = + convertLocationFacetValueToRequest(facetValue); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + values: [facetValueRequest], + type: 'location', + }), + }); + + const action = toggleAction({ + facetId, + selection: facetValue, + }); + const finalState = commerceFacetSetReducer(state, action); + + expect(finalState[facetId]?.request.preventAutoSelect).toBe(true); + }); + + it('sets #freezeCurrentValues to true', () => { + const facetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + }); + const facetValueRequest = + convertLocationFacetValueToRequest(facetValue); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + values: [facetValueRequest], + type: 'location', + }), + }); + + const action = toggleAction({ + facetId, + selection: facetValue, + }); + const finalState = commerceFacetSetReducer(state, action); + + expect(finalState[facetId]?.request.freezeCurrentValues).toBe(true); + }); + }); + + describe.each([ + { + facetValueState: 'selected' as FacetValueState, + toggleAction: toggleSelectLocationFacetValue, + }, + { + facetValueState: 'excluded' as FacetValueState, + toggleAction: toggleExcludeLocationFacetValue, + }, + ])( + 'when the facet value does not exist', + ({ + facetValueState, + toggleAction, + }: { + facetValueState: FacetValueState; + toggleAction: Function; + }) => { + it('replaces the first idle value with the new value', () => { + const newFacetValue = buildMockCommerceLocationFacetValue({ + value: 'TED', + state: facetValueState, + }); + + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({ + type: 'location', + values: [ + buildMockCommerceLocationFacetValue({ + value: 'active1', + state: facetValueState, + }), + buildMockCommerceLocationFacetValue({ + value: 'active2', + state: facetValueState, + }), + buildMockCommerceLocationFacetValue({ + value: 'idle1', + state: 'idle', + }), + buildMockCommerceLocationFacetValue({ + value: 'idle2', + state: 'idle', + }), + ], + }), + }); + + const action = toggleAction({ + facetId, + selection: newFacetValue, + }); + + const finalState = commerceFacetSetReducer(state, action); + expect( + ( + finalState[facetId]?.request + .values as LocationFacetValueRequest[] + ).indexOf(newFacetValue) + ).toBe(2); + expect(finalState[facetId]?.request.values.length).toBe(4); + }); + + it('sets #preventAutoSelect to true', () => { + state[facetId] = buildMockCommerceFacetSlice({ + request: buildMockCommerceFacetRequest({type: 'location'}), + }); + + const action = toggleAction({ + facetId, + selection: buildMockCommerceLocationFacetValue({value: 'TED'}), + }); + const finalState = commerceFacetSetReducer(state, action); + + expect(finalState[facetId]?.request.preventAutoSelect).toBe(true); + }); + } + ); + } + ); + it('dispatching #toggleSelectLocationFacetValue with an invalid id does not throw', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + const action = toggleSelectLocationFacetValue({ + facetId: '1', + selection: facetValue, + }); + + expect(() => commerceFacetSetReducer(state, action)).not.toThrow(); + }); + + it('dispatching #toggleSelectLocationFacetValue with an invalid facet type does not throw', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + const facet = buildMockCommerceFacetRequest({ + type: 'numericalRange', + values: [facetValue], + }); + state[facet.facetId] = buildMockCommerceFacetSlice({ + request: facet, + }); + const action = toggleSelectLocationFacetValue({ + facetId: facet.facetId, + selection: facetValue, + }); + + expect(() => commerceFacetSetReducer(state, action)).not.toThrow(); + }); + + it('dispatching #toggleExcludeLocationFacetValue with an invalid id does not throw', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + const action = toggleExcludeLocationFacetValue({ + facetId: '1', + selection: facetValue, + }); + + expect(() => commerceFacetSetReducer(state, action)).not.toThrow(); + }); + + it('dispatching #toggleExcludeLocationFacetValue with an invalid facet type does not throw', () => { + const facetValue = buildMockCommerceLocationFacetValue({value: 'TED'}); + const facet = buildMockCommerceFacetRequest({ + type: 'numericalRange', + values: [facetValue], + }); + state[facet.facetId] = buildMockCommerceFacetSlice({ + request: facet, + }); + const action = toggleExcludeLocationFacetValue({ + facetId: facet.facetId, + selection: facetValue, + }); + + expect(() => commerceFacetSetReducer(state, action)).not.toThrow(); + }); + }); + describe('for numericalRange facets', () => { describe.each([ { @@ -2365,6 +2669,7 @@ describe('commerceFacetSetReducer', () => { describe('#updateCoreFacetIsFieldExpanded', () => { describe.each([ {type: 'regular' as FacetType}, + {type: 'location' as FacetType}, {type: 'numericalRange' as FacetType}, {type: 'dateRange' as FacetType}, {type: 'hierarchical' as FacetType}, diff --git a/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.ts b/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.ts index f74142e0daa..25193bb8f84 100644 --- a/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.ts +++ b/packages/headless/src/features/commerce/facets/facet-set/facet-set-slice.ts @@ -44,6 +44,10 @@ import { executeCommerceFieldSuggest, getFacetIdWithCommerceFieldSuggestionNamespace, } from '../facet-search-set/commerce-facet-search-actions.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../location-facet/location-facet-actions.js'; import { toggleExcludeNumericFacetValue, toggleSelectNumericFacetValue, @@ -72,8 +76,10 @@ import { NumericFacetRequest, DateFacetRequest, CategoryFacetRequest, + LocationFacetRequest, + LocationFacetValueRequest, } from './interfaces/request.js'; -import {CategoryFacetValue} from './interfaces/response.js'; +import {CategoryFacetValue, LocationFacetValue} from './interfaces/response.js'; import {AnyFacetResponse} from './interfaces/response.js'; export const commerceFacetSetReducer = createReducer( @@ -122,6 +128,27 @@ export const commerceFacetSetReducer = createReducer( updateExistingFacetValueState(existingValue, 'select'); facetRequest.freezeCurrentValues = true; }) + .addCase(toggleSelectLocationFacetValue, (state, action) => { + const {facetId, selection} = action.payload; + const facetRequest = state[facetId]?.request; + + if (!facetRequest || !ensureLocationFacetRequest(facetRequest)) { + return; + } + + facetRequest.preventAutoSelect = true; + + const existingValue = facetRequest.values.find( + (req) => req.value === selection.value + ); + if (!existingValue) { + insertNewValue(facetRequest, selection); + return; + } + + updateExistingFacetValueState(existingValue, 'select'); + facetRequest.freezeCurrentValues = true; + }) .addCase(toggleSelectNumericFacetValue, (state, action) => { const {facetId, selection} = action.payload; const facetRequest = state[facetId]?.request; @@ -223,6 +250,27 @@ export const commerceFacetSetReducer = createReducer( updateExistingFacetValueState(existingValue, 'exclude'); facetRequest.freezeCurrentValues = true; }) + .addCase(toggleExcludeLocationFacetValue, (state, action) => { + const {facetId, selection} = action.payload; + const facetRequest = state[facetId]?.request; + + if (!facetRequest || !ensureLocationFacetRequest(facetRequest)) { + return; + } + + facetRequest.preventAutoSelect = true; + + const existingValue = facetRequest.values.find( + (req) => req.value === selection.value + ); + if (!existingValue) { + insertNewValue(facetRequest, selection); + return; + } + + updateExistingFacetValueState(existingValue, 'exclude'); + facetRequest.freezeCurrentValues = true; + }) .addCase(toggleExcludeNumericFacetValue, (state, action) => { const {facetId, selection} = action.payload; const facetRequest = state[facetId]?.request; @@ -450,6 +498,12 @@ function ensureRegularFacetRequest( return facetRequest.type === 'regular'; } +function ensureLocationFacetRequest( + facetRequest: AnyFacetRequest +): facetRequest is LocationFacetRequest { + return facetRequest.type === 'location'; +} + function ensureNumericFacetRequest( facetRequest: AnyFacetRequest ): facetRequest is NumericFacetRequest { @@ -533,7 +587,10 @@ function ensurePathAndReturnChildren( } function updateExistingFacetValueState( existingFacetValue: WritableDraft< - FacetValueRequest | NumericRangeRequest | DateRangeRequest + | FacetValueRequest + | LocationFacetValueRequest + | NumericRangeRequest + | DateRangeRequest >, toggleAction: 'select' | 'exclude' ) { @@ -613,6 +670,8 @@ function getFacetRequestValuesFromFacetResponse( : facetResponse.values.map(convertCategoryFacetValueToRequest); case 'regular': return facetResponse.values.map(convertFacetValueToRequest); + case 'location': + return facetResponse.values.map(convertLocationFacetValueToRequest); default: return; } @@ -635,6 +694,14 @@ export function convertCategoryFacetValueToRequest( }; } +export function convertLocationFacetValueToRequest( + facetValue: LocationFacetValue +): LocationFacetValueRequest { + const {value, state} = facetValue; + + return {value, state}; +} + function insertNewValue( facetRequest: AnyFacetRequest, facetValue: AnyFacetValueRequest diff --git a/packages/headless/src/features/commerce/facets/facet-set/interfaces/common.ts b/packages/headless/src/features/commerce/facets/facet-set/interfaces/common.ts index 67f2563fe21..42d7526900b 100644 --- a/packages/headless/src/features/commerce/facets/facet-set/interfaces/common.ts +++ b/packages/headless/src/features/commerce/facets/facet-set/interfaces/common.ts @@ -19,4 +19,5 @@ export type FacetType = | 'regular' | 'dateRange' | 'numericalRange' - | 'hierarchical'; + | 'hierarchical' + | 'location'; diff --git a/packages/headless/src/features/commerce/facets/facet-set/interfaces/request.ts b/packages/headless/src/features/commerce/facets/facet-set/interfaces/request.ts index ebe344f3a27..ed34d45d8ec 100644 --- a/packages/headless/src/features/commerce/facets/facet-set/interfaces/request.ts +++ b/packages/headless/src/features/commerce/facets/facet-set/interfaces/request.ts @@ -41,6 +41,13 @@ export type RegularFacetRequest = BaseCommerceFacetRequest< 'regular' >; +export type LocationFacetValueRequest = FacetValueRequest; + +export type LocationFacetRequest = BaseCommerceFacetRequest< + LocationFacetValueRequest, + 'location' +>; + export type BaseCommerceFacetRequest = Pick< FacetRequest, | 'facetId' @@ -59,6 +66,7 @@ export type BaseCommerceFacetRequest = Pick< export type AnyFacetValueRequest = | FacetValueRequest + | LocationFacetValueRequest | CategoryFacetValueRequest | NumericRangeRequest | DateRangeRequest; diff --git a/packages/headless/src/features/commerce/facets/facet-set/interfaces/response.ts b/packages/headless/src/features/commerce/facets/facet-set/interfaces/response.ts index 6ade7b0b585..505973ea6cf 100644 --- a/packages/headless/src/features/commerce/facets/facet-set/interfaces/response.ts +++ b/packages/headless/src/features/commerce/facets/facet-set/interfaces/response.ts @@ -39,6 +39,15 @@ export interface RegularFacetValue extends BaseFacetValue { value: string; } +export type LocationFacetResponse = BaseFacetResponse< + LocationFacetValue, + 'location' +>; + +export interface LocationFacetValue extends BaseFacetValue { + value: string; +} + export interface RangeFacetValue extends BaseFacetValue { start: T; end: T; @@ -70,6 +79,7 @@ export interface BaseFacetValue { export type AnyFacetValueResponse = | RegularFacetValue + | LocationFacetValue | NumericFacetValue | DateFacetValue | CategoryFacetValue; @@ -83,7 +93,9 @@ type MappedFacetResponse = { ? DateFacetResponse : T extends 'hierarchical' ? CategoryFacetResponse - : never; + : T extends 'location' + ? LocationFacetResponse + : never; }; export type AnyFacetResponse = MappedFacetResponse[FacetType]; diff --git a/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions.ts b/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions.ts new file mode 100644 index 00000000000..04db303b72c --- /dev/null +++ b/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions.ts @@ -0,0 +1,29 @@ +import {RecordValue} from '@coveo/bueno'; +import {createAction} from '@reduxjs/toolkit'; +import { + requiredNonEmptyString, + validatePayload, +} from '../../../../utils/validate-payload.js'; +import {facetValueDefinition} from '../../../facets/facet-set/facet-set-validate-payload.js'; +import { + ToggleExcludeFacetValuePayload, + ToggleSelectFacetValuePayload, +} from '../regular-facet/regular-facet-actions.js'; + +export const toggleExcludeLocationFacetValue = createAction( + 'commerce/facets/locationFacet/toggleExcludeValue', + (payload: ToggleExcludeFacetValuePayload) => + validatePayload(payload, { + facetId: requiredNonEmptyString, + selection: new RecordValue({values: facetValueDefinition}), + }) +); + +export const toggleSelectLocationFacetValue = createAction( + 'commerce/facets/locationFacet/toggleSelectValue', + (payload: ToggleSelectFacetValuePayload) => + validatePayload(payload, { + facetId: requiredNonEmptyString, + selection: new RecordValue({values: facetValueDefinition}), + }) +); diff --git a/packages/headless/src/features/commerce/pagination/pagination-slice.test.ts b/packages/headless/src/features/commerce/pagination/pagination-slice.test.ts index ef7374d0037..b4dca0520c6 100644 --- a/packages/headless/src/features/commerce/pagination/pagination-slice.test.ts +++ b/packages/headless/src/features/commerce/pagination/pagination-slice.test.ts @@ -11,6 +11,10 @@ import { toggleExcludeDateFacetValue, toggleSelectDateFacetValue, } from '../facets/date-facet/date-facet-actions.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../facets/location-facet/location-facet-actions.js'; import { toggleExcludeNumericFacetValue, toggleSelectNumericFacetValue, @@ -269,6 +273,14 @@ describe('pagination slice', () => { actionName: '#toggleExcludeFacetValue', action: toggleExcludeFacetValue, }, + { + actionName: '#toggleSelectLocationFacetValue', + action: toggleSelectLocationFacetValue, + }, + { + actionName: '#toggleExcludeLocationFacetValue', + action: toggleExcludeLocationFacetValue, + }, { actionName: '#toggleSelectNumericFacetValue', action: toggleSelectNumericFacetValue, diff --git a/packages/headless/src/features/commerce/pagination/pagination-slice.ts b/packages/headless/src/features/commerce/pagination/pagination-slice.ts index daf24ac1a3b..b7adff645b0 100644 --- a/packages/headless/src/features/commerce/pagination/pagination-slice.ts +++ b/packages/headless/src/features/commerce/pagination/pagination-slice.ts @@ -9,6 +9,10 @@ import { toggleExcludeDateFacetValue, toggleSelectDateFacetValue, } from '../facets/date-facet/date-facet-actions.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from '../facets/location-facet/location-facet-actions.js'; import { toggleExcludeNumericFacetValue, toggleSelectNumericFacetValue, @@ -109,6 +113,8 @@ export const paginationReducer = createReducer( .addCase(deselectAllValuesInCoreFacet, handlePaginationReset) .addCase(toggleSelectFacetValue, handlePaginationReset) .addCase(toggleExcludeFacetValue, handlePaginationReset) + .addCase(toggleSelectLocationFacetValue, handlePaginationReset) + .addCase(toggleExcludeLocationFacetValue, handlePaginationReset) .addCase(toggleSelectNumericFacetValue, handlePaginationReset) .addCase(toggleExcludeNumericFacetValue, handlePaginationReset) .addCase(toggleSelectDateFacetValue, handlePaginationReset) diff --git a/packages/headless/src/test/mock-commerce-facet-response.ts b/packages/headless/src/test/mock-commerce-facet-response.ts index 0fb0d568de9..9c188778637 100644 --- a/packages/headless/src/test/mock-commerce-facet-response.ts +++ b/packages/headless/src/test/mock-commerce-facet-response.ts @@ -4,6 +4,7 @@ import { DateFacetResponse, AnyFacetResponse, CategoryFacetResponse, + LocationFacetResponse, } from '../features/commerce/facets/facet-set/interfaces/response.js'; function getMockBaseCommerceFacetResponse(): Omit< @@ -32,6 +33,17 @@ export function buildMockCommerceRegularFacetResponse( }; } +export function buildMockCommerceLocationFacetResponse( + config: Partial = {} +): LocationFacetResponse { + return { + ...getMockBaseCommerceFacetResponse(), + type: 'location', + values: [], + ...config, + }; +} + export function buildMockCommerceNumericFacetResponse( config: Partial = {} ): NumericFacetResponse { diff --git a/packages/headless/src/test/mock-commerce-facet-value.ts b/packages/headless/src/test/mock-commerce-facet-value.ts index dffd169cd16..09130506392 100644 --- a/packages/headless/src/test/mock-commerce-facet-value.ts +++ b/packages/headless/src/test/mock-commerce-facet-value.ts @@ -3,6 +3,7 @@ import { NumericFacetValue, DateFacetValue, CategoryFacetValue, + LocationFacetValue, } from '../features/commerce/facets/facet-set/interfaces/response.js'; export function buildMockCommerceRegularFacetValue( @@ -19,6 +20,20 @@ export function buildMockCommerceRegularFacetValue( }; } +export function buildMockCommerceLocationFacetValue( + config: Partial = {} +): LocationFacetValue { + return { + value: '', + state: 'idle', + numberOfResults: 0, + isAutoSelected: false, + isSuggested: false, + moreValuesAvailable: false, + ...config, + }; +} + export function buildMockCommerceNumericFacetValue( config: Partial = {} ): NumericFacetValue { From 7b9144df2d88a6d28065d6eca8e4cf27c592bf7d Mon Sep 17 00:00:00 2001 From: Louis Bompart Date: Wed, 23 Oct 2024 14:31:56 -0400 Subject: [PATCH 04/57] chore: promote the v3 branch when publishing on v3 (#4585) Sames as #4584 for v3 Technically, we could "just fix" the script. However, I think it's best to make it "fit" with the rest of our release scripts in its code architecture while keeping the same functionality https://coveord.atlassian.net/browse/KIT-3668 --- package-lock.json | 30 ++++++------ .../projects/atomic-angular/project.json | 2 +- packages/atomic-hosted-page/package.json | 2 +- packages/atomic-react/package.json | 2 +- packages/atomic/package.json | 2 +- packages/bueno/package.json | 2 +- packages/headless-react/package.json | 2 +- packages/headless/package.json | 2 +- packages/quantic/package.json | 2 +- scripts/deploy/update-npm-tag.mjs | 49 ------------------- utils/release/common/constants.mjs | 4 ++ utils/release/package.json | 3 +- utils/release/promote-npm-tag-to-latest.mjs | 25 ++++++++++ 13 files changed, 54 insertions(+), 73 deletions(-) delete mode 100644 scripts/deploy/update-npm-tag.mjs create mode 100755 utils/release/promote-npm-tag-to-latest.mjs diff --git a/package-lock.json b/package-lock.json index b20c3ea94b3..d6cd7332cbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5052,6 +5052,20 @@ "resolved": "packages/rollup-plugin-replace-with-ast", "link": true }, + "node_modules/@coveo/semantic-monorepo-tools": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@coveo/semantic-monorepo-tools/-/semantic-monorepo-tools-2.5.0.tgz", + "integrity": "sha512-LtFtf1nJfS4mjeOKtKsDak30qmh69H159EXzJJ0C9ubF4YNdRN9MdTRc+7nb1IgiVK+um4huEWkM5S9UkGkQpQ==", + "license": "Apache-2.0", + "dependencies": { + "conventional-changelog-writer": "^7.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.3.3", + "git-raw-commits": "^4.0.0", + "semver": "^7.3.7", + "tempfile": "^5.0.0" + } + }, "node_modules/@cspell/cspell-bundled-dicts": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.12.1.tgz", @@ -65468,7 +65482,7 @@ "name": "@coveo/release", "version": "1.0.0", "dependencies": { - "@coveo/semantic-monorepo-tools": "2.4.61", + "@coveo/semantic-monorepo-tools": "2.5.0", "@npmcli/arborist": "7.5.4", "@octokit/auth-app": "6.1.1", "async-retry": "1.3.3", @@ -65485,20 +65499,6 @@ "typescript": "5.4.5" } }, - "utils/release/node_modules/@coveo/semantic-monorepo-tools": { - "version": "2.4.61", - "resolved": "https://registry.npmjs.org/@coveo/semantic-monorepo-tools/-/semantic-monorepo-tools-2.4.61.tgz", - "integrity": "sha512-zYOUlRsEXc0p5/rhHUa3ApVb3SXIh50iwRbHQVwTNoaNOTKlp5YDxX8rzpqVVEb1TlfV0GYAaZ2WaVZ9TLKwxQ==", - "license": "Apache-2.0", - "dependencies": { - "conventional-changelog-writer": "^7.0.0", - "conventional-commits-parser": "^5.0.0", - "debug": "^4.3.3", - "git-raw-commits": "^4.0.0", - "semver": "^7.3.7", - "tempfile": "^5.0.0" - } - }, "utils/release/node_modules/@npmcli/arborist": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", diff --git a/packages/atomic-angular/projects/atomic-angular/project.json b/packages/atomic-angular/projects/atomic-angular/project.json index 3fdd499c3a8..cb7e638b35e 100644 --- a/packages/atomic-angular/projects/atomic-angular/project.json +++ b/packages/atomic-angular/projects/atomic-angular/project.json @@ -9,7 +9,7 @@ "outputs": [], "executor": "nx:run-commands", "options": { - "command": "node ../../../../scripts/deploy/update-npm-tag.mjs latest", + "command": "npm run-script -w=@coveo/release promote-npm-prod", "cwd": "packages/atomic-angular/projects/atomic-angular" } } diff --git a/packages/atomic-hosted-page/package.json b/packages/atomic-hosted-page/package.json index f8b5b44a613..080edc90fd4 100644 --- a/packages/atomic-hosted-page/package.json +++ b/packages/atomic-hosted-page/package.json @@ -27,7 +27,7 @@ "validate:definitions": "tsc --noEmit --esModuleInterop --skipLibCheck ./dist/types/components.d.ts", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest" + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod" }, "dependencies": { "@coveo/bueno": "1.0.1", diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index 1a588ee903b..aa67cb59b7f 100644 --- a/packages/atomic-react/package.json +++ b/packages/atomic-react/package.json @@ -18,7 +18,7 @@ "build:bundles": "concurrently \"npm run build:bundles:esm\" \"npm run build:bundles:iife-cjs\"", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest", + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod", "build:assets": "ncp ../atomic/dist/atomic/assets dist/assets && ncp ../atomic/dist/atomic/lang dist/lang " }, "main": "./dist/cjs/atomic-react.cjs", diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 3027abaefca..1009084f0c3 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -62,7 +62,7 @@ "e2e:insight:watch": "cypress open --config-file cypress-insight-panel.config.mjs --browser chrome", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest", + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod", "validate:definitions": "tsc --noEmit --esModuleInterop --skipLibCheck ./dist/types/components.d.ts" }, "dependencies": { diff --git a/packages/bueno/package.json b/packages/bueno/package.json index 1cec4261d1b..091317c193f 100644 --- a/packages/bueno/package.json +++ b/packages/bueno/package.json @@ -27,7 +27,7 @@ "test:watch": "jest --watch --colors --no-cache", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest" + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod" }, "devDependencies": { "@coveo/release": "1.0.0", diff --git a/packages/headless-react/package.json b/packages/headless-react/package.json index 78535a8216d..50c1e4f60e6 100644 --- a/packages/headless-react/package.json +++ b/packages/headless-react/package.json @@ -30,7 +30,7 @@ "lint": "eslint .; publint", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest" + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod" }, "dependencies": { "@coveo/headless": "3.4.0" diff --git a/packages/headless/package.json b/packages/headless/package.json index ae52d4511b8..263c21f5a5d 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -135,7 +135,7 @@ "integration-test:watch": "vitest --poolOptions.threads.singleThread src/integration-tests/**", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest", + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod", "build:doc": "npm run build:doc:extract && npm run build:doc:parse", "build:doc:extract": "node ./scripts/extract-documentation.mjs", "build:doc:parse": "ts-node --project ./doc-parser/tsconfig.build.json ./doc-parser/doc-parser.ts" diff --git a/packages/quantic/package.json b/packages/quantic/package.json index 3c9e47fb901..6ec19618e4f 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -40,7 +40,7 @@ "promote:sfdx:ci": "npm run publish:sfdx -- --promote --ci", "publish:npm": "npm run-script -w=@coveo/release npm-publish", "publish:bump": "npm run-script -w=@coveo/release bump", - "promote:npm:latest": "node ../../scripts/deploy/update-npm-tag.mjs latest", + "promote:npm:latest": "npm run-script -w=@coveo/release promote-npm-prod", "preinstall": "node scripts/npm/check-sfdx-project.js", "postinstall": "node scripts/npm/setup-quantic.js" }, diff --git a/scripts/deploy/update-npm-tag.mjs b/scripts/deploy/update-npm-tag.mjs deleted file mode 100644 index 50738107cd8..00000000000 --- a/scripts/deploy/update-npm-tag.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import {execute} from '../exec.mjs'; -import {getPackageManifestFromPackagePath} from '../packages.mjs'; - -const pkg = getPackageManifestFromPackagePath(process.cwd()); - -async function updateNpmTag(packageName, version) { - const tag = process.argv[2]; - const latestVersion = await getLatestVersion(packageName); - - if (!isGreaterThanLatestVersion(version, latestVersion)) { - console.log( - `skipping tag update for ${packageName} because version "${version}" is not greater than latest version "${latestVersion}".` - ); - return; - } - - console.log(`updating ${packageName}@${version} to ${tag}.`); - await execute('npm', ['dist-tag', 'add', `${packageName}@${version}`, tag]); -} - -async function getLatestVersion(packageName) { - const res = await execute('npm', ['view', packageName, 'version']); - return res.trim(); -} - -function isGreaterThanLatestVersion(version, latestVersion) { - const candidate = parseVersion(version); - const latest = parseVersion(latestVersion); - - return isCandidateGreaterThanLatestVersion(candidate, latest, 0); -} - -function parseVersion(version) { - return version.split('.').map((num) => parseInt(num, 10)); -} - -function isCandidateGreaterThanLatestVersion(candidate, latest, i) { - if (i >= candidate.length) { - return false; - } - - if (candidate[i] === latest[i]) { - return isCandidateGreaterThanLatestVersion(candidate, latest, i + 1); - } - - return candidate[i] > latest[i]; -} - -updateNpmTag(pkg.name, pkg.version); diff --git a/utils/release/common/constants.mjs b/utils/release/common/constants.mjs index 9b348a34525..ee560ac9341 100644 --- a/utils/release/common/constants.mjs +++ b/utils/release/common/constants.mjs @@ -20,3 +20,7 @@ export const RELEASER_AUTH_SECRETS = { clientSecret: process.env.RELEASER_CLIENT_SECRET, installationId: process.env.RELEASER_INSTALLATION_ID, }; + +export const NPM_LATEST_TAG = 'latest'; +export const NPM_BETA_TAG = 'beta'; +export const NPM_ALPHA_TAG = 'alpha'; diff --git a/utils/release/package.json b/utils/release/package.json index 8a7cc14c259..6952de1f0f3 100644 --- a/utils/release/package.json +++ b/utils/release/package.json @@ -5,7 +5,7 @@ "version": "1.0.0", "type": "module", "dependencies": { - "@coveo/semantic-monorepo-tools": "2.4.61", + "@coveo/semantic-monorepo-tools": "2.5.0", "@npmcli/arborist": "7.5.4", "@octokit/auth-app": "6.1.1", "async-retry": "1.3.3", @@ -22,6 +22,7 @@ "typescript": "5.4.5" }, "scripts": { + "promote-npm-prod": "./promote-npm-tag-to-latest.mjs", "git-lock": "./git-lock.mjs", "bump": "./bump-package.mjs", "npm-publish": "./npm-publish-package.mjs", diff --git a/utils/release/promote-npm-tag-to-latest.mjs b/utils/release/promote-npm-tag-to-latest.mjs new file mode 100755 index 00000000000..3e33bfa26d8 --- /dev/null +++ b/utils/release/promote-npm-tag-to-latest.mjs @@ -0,0 +1,25 @@ +#!/usr/bin/env node +import {describeNpmTag, npmSetTag} from '@coveo/semantic-monorepo-tools'; +import {readFileSync} from 'node:fs'; +import {gt} from 'semver'; +import {NPM_LATEST_TAG} from './common/constants.mjs'; + +if (!process.env.INIT_CWD) { + throw new Error('Should be called using npm run-script'); +} +process.chdir(process.env.INIT_CWD); + +const {name, version} = JSON.parse( + readFileSync('package.json', {encoding: 'utf-8'}) +); + +const publishedVersion = await describeNpmTag(name, NPM_LATEST_TAG); + +if (gt(publishedVersion, version)) { + console.log( + `skipping tag update for ${name} because version "${version}" is not greater than latest version "${publishedVersion}".` + ); + process.exit(1); +} + +await npmSetTag(name, version, NPM_LATEST_TAG); From 05cb49748fac382a792fcc7ea5a7815462ea9444 Mon Sep 17 00:00:00 2001 From: Nico Labarre Date: Wed, 23 Oct 2024 14:54:32 -0400 Subject: [PATCH 05/57] feat(commerce): add location facets actions loader (#4569) See: https://github.com/coveo/ui-kit/pull/4562 [COMHUB-247] [COMHUB-247]: https://coveord.atlassian.net/browse/COMHUB-247?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../headless/doc-parser/use-cases/commerce.ts | 3 ++ packages/headless/src/commerce.index.ts | 2 +- .../location-facet-actions-loader.ts | 54 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/headless/src/features/commerce/facets/location-facet/location-facet-actions-loader.ts diff --git a/packages/headless/doc-parser/use-cases/commerce.ts b/packages/headless/doc-parser/use-cases/commerce.ts index 7a6b7ef0254..c8891ebe802 100644 --- a/packages/headless/doc-parser/use-cases/commerce.ts +++ b/packages/headless/doc-parser/use-cases/commerce.ts @@ -138,6 +138,9 @@ const actionLoaders: ActionLoaderConfiguration[] = [ { initializer: 'loadRegularFacetActions', }, + { + initializer: 'loadLocationFacetActions', + }, // TODO: KIT-3422 - Uncomment when ready to generate typedoc docs // { // initializer: 'loadQuerySetActions', diff --git a/packages/headless/src/commerce.index.ts b/packages/headless/src/commerce.index.ts index 4bb4ee6ab96..e07be674a09 100644 --- a/packages/headless/src/commerce.index.ts +++ b/packages/headless/src/commerce.index.ts @@ -51,7 +51,7 @@ export * from './features/commerce/sort/sort-actions-loader.js'; export * from './features/commerce/facets/core-facet/core-facet-actions-loader.js'; export * from './features/commerce/facets/category-facet/category-facet-actions-loader.js'; export * from './features/commerce/facets/regular-facet/regular-facet-actions-loader.js'; -// TODO COMHUB-247 export location facets actions loader +export * from './features/commerce/facets/location-facet/location-facet-actions-loader.js'; export * from './features/commerce/facets/date-facet/date-facet-actions-loader.js'; export * from './features/commerce/facets/numeric-facet/numeric-facet-actions-loader.js'; export * from './features/commerce/query-set/query-set-actions-loader.js'; diff --git a/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions-loader.ts b/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions-loader.ts new file mode 100644 index 00000000000..74c25fa3ced --- /dev/null +++ b/packages/headless/src/features/commerce/facets/location-facet/location-facet-actions-loader.ts @@ -0,0 +1,54 @@ +import {PayloadAction} from '@reduxjs/toolkit'; +import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine.js'; +import {commerceFacetSetReducer as commerceFacetSet} from '../facet-set/facet-set-slice.js'; +import { + ToggleExcludeFacetValuePayload, + ToggleSelectFacetValuePayload, +} from '../regular-facet/regular-facet-actions.js'; +import { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, +} from './location-facet-actions.js'; + +export type {ToggleExcludeFacetValuePayload, ToggleSelectFacetValuePayload}; + +/** + * The location facet action creators. + */ +export interface LocationFacetActionCreators { + /** + * Toggles the exclusion of a given location facet value. + * + * @param payload - The action creator payload. + * @returns A dispatchable action. + */ + toggleExcludeLocationFacetValue( + payload: ToggleExcludeFacetValuePayload + ): PayloadAction; + + /** + * Toggles the selection of a given location facet value. + * + * @param payload - The action creator payload. + * @returns A dispatchable action. + */ + toggleSelectLocationFacetValue( + payload: ToggleSelectFacetValuePayload + ): PayloadAction; +} + +/** + * Loads the commerce facet set reducer and returns the available location facet action creators. + * + * @param engine - The commerce engine. + * @returns An object holding the location facet action creators. + */ +export function loadLocationFacetActions( + engine: CommerceEngine +): LocationFacetActionCreators { + engine.addReducers({commerceFacetSet}); + return { + toggleExcludeLocationFacetValue, + toggleSelectLocationFacetValue, + }; +} From 9ae81be667b5cba01310d3d21c06f29987711530 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Wed, 23 Oct 2024 15:43:43 -0400 Subject: [PATCH 06/57] test(atomic): fix script to scope E2E tests (#4554) ## 2 issues fixed: 1. Incorrect Error Handling: When the script detects a file change from another package (e.g., headless/), it throws an error but does not set the `testsToRun` variable. This causes the CI to fail. 2. False Positive Detection: The script incorrectly detects changes in `headless-react` as changes in `headless`, leading to the execution of all tests unnecessarily. https://coveord.atlassian.net/browse/KIT-3667 --------- Co-authored-by: developer-experience-bot[bot] <91079284+developer-experience-bot[bot]@users.noreply.github.com> Co-authored-by: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> --- scripts/ci/find-tests.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/ci/find-tests.mjs b/scripts/ci/find-tests.mjs index a8d0b11bdcb..ccf609fc719 100644 --- a/scripts/ci/find-tests.mjs +++ b/scripts/ci/find-tests.mjs @@ -92,12 +92,12 @@ function determineTestFilesToRun(changedFiles, testDependencies) { function ensureIsNotCoveoPackage(file) { if (dependsOnCoveoPackage(file)) { - throw new Error('Change detected in an different Coveo package.'); + throw new Error(`Change detected in an different Coveo package: ${file}`); } } function dependsOnCoveoPackage(file) { - const externalPackages = ['packages/headless', 'packages/bueno']; + const externalPackages = ['packages/headless/', 'packages/bueno/']; for (const pkg of externalPackages) { if (file.includes(pkg)) { return true; @@ -122,4 +122,5 @@ try { } } catch (error) { console.warn(error?.message || error); + setOutput(outputName, ''); // Passing an empty string will run all tests. } From a114213be2b0e787d0bcec1ae31ca903c5941ce8 Mon Sep 17 00:00:00 2001 From: Simon Milord Date: Wed, 23 Oct 2024 15:47:38 -0400 Subject: [PATCH 07/57] feat(quantic): added E2E and unit tests for insight notify trigger in quantic (#4528) [SFINT-5766](https://coveord.atlassian.net/browse/SFINT-5766) ### IN THIS PR: - Modified E2E tests to support insight use case for notify triggers in Cypress - Added unit tests for the `quanticNotifications` component [SFINT-5766]: https://coveord.atlassian.net/browse/SFINT-5766?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: developer-experience-bot[bot] <91079284+developer-experience-bot[bot]@users.noreply.github.com> --- .../notifications/notifications.cypress.ts | 56 +++-- .../quantic/cypress/page-objects/search.ts | 9 +- .../exampleQuanticNotifications.html | 5 +- .../exampleQuanticNotifications.js | 10 +- .../__tests__/quanticNotifications.test.js | 227 ++++++++++++++++++ .../quanticNotifications.html | 2 +- .../quanticNotifications.js | 3 +- 7 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js diff --git a/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts b/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts index b0158daa091..f64e44fe7dd 100644 --- a/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts +++ b/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts @@ -1,41 +1,59 @@ import {configure} from '../../../page-objects/configurator'; +import {performSearch} from '../../../page-objects/actions/action-perform-search'; import { getQueryAlias, interceptSearch, mockSearchWithNotifyTrigger, } from '../../../page-objects/search'; import {NotificationsExpectations as Expect} from './notifications-expectations'; - +import { + useCaseParamTest, + useCaseEnum, + InsightInterfaceExpectations as InsightInterfaceExpect, +} from '../../../page-objects/use-case'; const exampleNotifications = ['Notification one', 'Notification two']; +interface NotificationsOptions { + useCase: string; +} + describe('quantic-notifications', () => { const pageUrl = 's/quantic-notifications'; - function visitNotifications() { + function visitNotifications(options: Partial) { interceptSearch(); cy.visit(pageUrl); - configure(); + configure(options); + if (options.useCase === useCaseEnum.insight) { + InsightInterfaceExpect.isInitialized(); + performSearch(); + } } - describe('when no notification is fired by the pipeline trigger', () => { - it('should not render any notification', () => { - visitNotifications(); + useCaseParamTest.forEach((param) => { + describe(param.label, () => { + describe('when no notification is fired by the pipeline trigger', () => { + it('should not render any notification', () => { + visitNotifications({useCase: param.useCase}); + mockSearchWithNotifyTrigger(param.useCase, []); - cy.wait(getQueryAlias()); - Expect.displayNotifications(false); - }); - }); + cy.wait(getQueryAlias(param.useCase)); + Expect.displayNotifications(false); + }); + }); - describe('when some notifications are fired by the pipeline trigger', () => { - it('should render the notifications', () => { - visitNotifications(); - mockSearchWithNotifyTrigger('search', exampleNotifications); + describe('when some notifications are fired by the pipeline trigger', () => { + it('should render the notifications', () => { + mockSearchWithNotifyTrigger(param.useCase, exampleNotifications); + visitNotifications({useCase: param.useCase}); - cy.wait(getQueryAlias()); - Expect.displayNotifications(true); - Expect.logQueryPipelineTriggersNotification(exampleNotifications); - exampleNotifications.forEach((notification, index) => { - Expect.notificationContains(index, notification); + cy.wait(getQueryAlias(param.useCase)); + Expect.displayNotifications(true); + Expect.logQueryPipelineTriggersNotification(exampleNotifications); + exampleNotifications.forEach((notification, index) => { + Expect.notificationContains(index, notification); + }); + }); }); }); }); diff --git a/packages/quantic/cypress/page-objects/search.ts b/packages/quantic/cypress/page-objects/search.ts index ddec07b06e4..d55f7c943b4 100644 --- a/packages/quantic/cypress/page-objects/search.ts +++ b/packages/quantic/cypress/page-objects/search.ts @@ -645,15 +645,20 @@ export function mockSearchWithNotifyTrigger( useCase: string, notifications: string[] ) { + const InterceptAliasesToUse = + useCase === useCaseEnum.insight + ? InterceptAliases.Insight + : InterceptAliases.Search; + cy.intercept(getRoute(useCase), (req) => { req.continue((res) => { - res.body.triggers = notifications.map((notification) => ({ + res.body.triggers = notifications?.map((notification) => ({ type: 'notify', content: notification, })); res.send(); }); - }).as(InterceptAliases.Search.substring(1)); + }).as(InterceptAliasesToUse.substring(1)); } export function mockQuerySuggestions(suggestions: string[]) { diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html index dbe325ec081..a2220d9c6bd 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html @@ -3,11 +3,12 @@
+
- + - + \ No newline at end of file diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js index 1bc1e63e32e..75daa7f1688 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js @@ -8,7 +8,15 @@ export default class ExampleQuanticNotifications extends LightningElement { pageTitle = 'Quantic Notifications'; pageDescription = 'component is responsible for displaying notifications generated by the Coveo Search API.'; - options = []; + options = [ + { + attribute: 'useCase', + label: 'Use Case', + description: + 'Define which use case to test. Possible values are: search, insights', + defaultValue: 'search', + }, + ]; get notConfigured() { return !this.isConfigured; diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js new file mode 100644 index 00000000000..bd8e14e7d3b --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js @@ -0,0 +1,227 @@ +/* eslint-disable no-import-assign */ +// @ts-ignore +import QuanticNotifications from 'c/quanticNotifications'; +// @ts-ignore +import {createElement} from 'lwc'; +import * as mockHeadlessLoader from 'c/quanticHeadlessLoader'; +import {AriaLiveRegion} from 'c/quanticUtils'; + +jest.mock('c/quanticHeadlessLoader'); +jest.mock('c/quanticUtils'); + +const exampleNotifications = ['notification1', 'notification2']; + +let notificationsState = { + notifications: exampleNotifications, +}; +let isInitialized = false; + +const exampleEngine = { + id: 'mock engine', +}; + +const functionsMocks = { + buildNotifyTrigger: jest.fn(() => ({ + state: notificationsState, + subscribe: functionsMocks.subscribe, + })), + dispatchMessage: jest.fn(() => {}), + subscribe: jest.fn((cb) => { + cb(); + return functionsMocks.unsubscribe; + }), + unsubscribe: jest.fn(() => {}), +}; + +// @ts-ignore +AriaLiveRegion.mockImplementation(() => { + return { + dispatchMessage: functionsMocks.dispatchMessage, + }; +}); + +const selectors = { + notifications: '[data-test="notification"]', + initializationError: 'c-quantic-component-error', +}; + +const defaultOptions = { + engineId: 'exampleEngineId', +}; + +function createTestComponent(options = defaultOptions) { + prepareHeadlessState(); + + const element = createElement('c-quantic-notifications', { + is: QuanticNotifications, + }); + for (const [key, value] of Object.entries(options)) { + element[key] = value; + } + + document.body.appendChild(element); + return element; +} + +function prepareHeadlessState() { + // @ts-ignore + mockHeadlessLoader.getHeadlessBundle = () => { + return { + buildNotifyTrigger: functionsMocks.buildNotifyTrigger, + }; + }; +} + +// Helper function to wait until the microtask queue is empty. +function flushPromises() { + // eslint-disable-next-line @lwc/lwc/no-async-operation + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +function mockSuccessfulHeadlessInitialization() { + // @ts-ignore + mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => { + if (element instanceof QuanticNotifications && !isInitialized) { + isInitialized = true; + initialize(exampleEngine); + } + }; +} + +function mockErroneousHeadlessInitialization() { + // @ts-ignore + mockHeadlessLoader.initializeWithHeadless = (element) => { + if (element instanceof QuanticNotifications) { + element.setInitializationError(); + } + }; +} + +function cleanup() { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + isInitialized = false; +} + +describe('c-quantic-notifications', () => { + beforeAll(() => { + mockSuccessfulHeadlessInitialization(); + }); + + afterEach(() => { + cleanup(); + notificationsState = { + notifications: exampleNotifications, + }; + }); + + describe('when an error occurs during initialization', () => { + beforeEach(() => { + mockErroneousHeadlessInitialization(); + }); + + afterAll(() => { + mockSuccessfulHeadlessInitialization(); + }); + + it('should display the initialization error component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const initializationError = element.shadowRoot.querySelector( + selectors.initializationError + ); + + const notification = element.shadowRoot.querySelector( + selectors.notifications + ); + + expect(initializationError).not.toBeNull(); + expect(notification).toBeNull(); + }); + }); + + describe('component initialization', () => { + it('should build the controller with the proper paramters', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledTimes(1); + expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledWith( + exampleEngine + ); + }); + + it('should subscribe to the headless state changes', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); + }); + + it('should call AriaLiveRegion with the right parameters', async () => { + await createTestComponent(); + await flushPromises(); + + expect(AriaLiveRegion).toHaveBeenCalledTimes(1); + expect(AriaLiveRegion).toHaveBeenCalledWith( + 'notifications', + expect.anything() + ); + }); + }); + + describe('when the component is initialized successfully', () => { + describe('when some notifications are present in the state', () => { + it('should render the notifications component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const notifications = element.shadowRoot.querySelectorAll( + selectors.notifications + ); + + expect(notifications).not.toBeNull(); + expect(notifications.length).toBe(exampleNotifications.length); + notifications.forEach((notification, index) => { + expect(notification.textContent).toEqual(exampleNotifications[index]); + }); + }); + + it('should call dispatchMessage with the correct message', async () => { + await createTestComponent(); + await flushPromises(); + + const expectedMessage = + ' Notification 1: notification1 Notification 2: notification2'; + + expect(functionsMocks.dispatchMessage).toHaveBeenCalledTimes(1); + expect(functionsMocks.dispatchMessage).toHaveBeenCalledWith( + expectedMessage + ); + }); + }); + + describe('when no notifications are present in the state', () => { + beforeEach(() => { + notificationsState = { + notifications: [], + }; + }); + + it('should not render the notifications component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const notifications = element.shadowRoot.querySelectorAll( + selectors.notifications + ); + + expect(notifications.length).toEqual(0); + }); + }); + }); +}); diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html index 7a8742ef624..6b1621ec078 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html +++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html @@ -5,7 +5,7 @@ diff --git a/packages/quantic/jest.config.js b/packages/quantic/jest.config.js index d41019db77f..79116280322 100644 --- a/packages/quantic/jest.config.js +++ b/packages/quantic/jest.config.js @@ -29,6 +29,8 @@ module.exports = { '/force-app/main/default/lwc/quanticResultActionStyles/quanticResultActionStyles', '^c/searchBoxStyle$': '/force-app/main/default/lwc/searchBoxStyle/searchBoxStyle', + '^c/quanticFacetStyles$': + '/force-app/main/default/lwc/quanticFacetStyles/quanticFacetStyles', }, modulePathIgnorePatterns: ['.cache'], // add any custom configurations here From 91a2538742848e708234abde4e7f0ee14e80da6c Mon Sep 17 00:00:00 2001 From: Frederic Beaudoin Date: Tue, 29 Oct 2024 13:18:46 -0400 Subject: [PATCH 35/57] fix(atomic-react): render empty link container when display is other than grid (#4604) https://coveord.atlassian.net/browse/KIT-3561 At the moment, we're rendering the default link container (i.e., an AtomicProductLink) even on list and table display, making the entire result clickable. This should only be the case in grid display. **Before fix**: