diff --git a/.cspell.json b/.cspell.json index 2c202796f15..6680f0e6277 100644 --- a/.cspell.json +++ b/.cspell.json @@ -37,6 +37,7 @@ "coveo", "coveobueno", "coveointernaltesting", + "coveosearch", "coveotaggedword", "coversationid", "CRGA", diff --git a/.github/actions/e2e-atomic-angular/action.yml b/.github/actions/e2e-atomic-angular/action.yml index 3712085a4f8..a5aa8a9be57 100644 --- a/.github/actions/e2e-atomic-angular/action.yml +++ b/.github/actions/e2e-atomic-angular/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-atomic-insight-panel/action.yml b/.github/actions/e2e-atomic-insight-panel/action.yml index 04082a54fa8..b7d3ba0f054 100644 --- a/.github/actions/e2e-atomic-insight-panel/action.yml +++ b/.github/actions/e2e-atomic-insight-panel/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-atomic-next/action.yml b/.github/actions/e2e-atomic-next/action.yml index 1e89edc89a1..a6d3b2f153b 100644 --- a/.github/actions/e2e-atomic-next/action.yml +++ b/.github/actions/e2e-atomic-next/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-atomic-react/action.yml b/.github/actions/e2e-atomic-react/action.yml index 3f3994a5dcb..43e52330d71 100644 --- a/.github/actions/e2e-atomic-react/action.yml +++ b/.github/actions/e2e-atomic-react/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-headless-ssr-app-dev/action.yml b/.github/actions/e2e-headless-ssr-app-dev/action.yml index dfc771b2716..461a4c7e898 100644 --- a/.github/actions/e2e-headless-ssr-app-dev/action.yml +++ b/.github/actions/e2e-headless-ssr-app-dev/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-headless-ssr-app-prod/action.yml b/.github/actions/e2e-headless-ssr-app-prod/action.yml index 71daea3e6b6..dad890b9c05 100644 --- a/.github/actions/e2e-headless-ssr-app-prod/action.yml +++ b/.github/actions/e2e-headless-ssr-app-prod/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-headless-ssr-pages-dev/action.yml b/.github/actions/e2e-headless-ssr-pages-dev/action.yml index 66c8637c711..6f5cd634919 100644 --- a/.github/actions/e2e-headless-ssr-pages-dev/action.yml +++ b/.github/actions/e2e-headless-ssr-pages-dev/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-headless-ssr-pages-prod/action.yml b/.github/actions/e2e-headless-ssr-pages-prod/action.yml index 351ca95fc36..6aef64a27b8 100644 --- a/.github/actions/e2e-headless-ssr-pages-prod/action.yml +++ b/.github/actions/e2e-headless-ssr-pages-prod/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-iife/action.yml b/.github/actions/e2e-iife/action.yml index 5b2a1f8c7ae..acef814ff58 100644 --- a/.github/actions/e2e-iife/action.yml +++ b/.github/actions/e2e-iife/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/actions/e2e-stencil/action.yml b/.github/actions/e2e-stencil/action.yml index aa6051fac3d..94306a8f6ea 100644 --- a/.github/actions/e2e-stencil/action.yml +++ b/.github/actions/e2e-stencil/action.yml @@ -8,6 +8,8 @@ inputs: runs: using: composite steps: + - run: npx cypress install + shell: bash - uses: cypress-io/github-action@v5 name: Run Cypress with: diff --git a/.github/workflows/prbot.yml b/.github/workflows/prbot.yml index e6a26d19641..e44d6c36c34 100644 --- a/.github/workflows/prbot.yml +++ b/.github/workflows/prbot.yml @@ -70,6 +70,14 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: ./.github/actions/setup - run: npm test + package-compatibility: + name: 'Verify compatibility of packages' + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: ./.github/actions/setup + - run: npm run package-compatibility e2e-atomic-csp-test: name: 'Run e2e tests on Atomic CSP' needs: build @@ -86,8 +94,8 @@ jobs: maximumShards: 24 outputs: testsToRun: ${{ steps.determine-tests.outputs.testsToRun }} - shardIndex: ${{ steps.set-matrix.outputs.shardIndex }} - shardTotal: ${{ steps.set-matrix.outputs.shardTotal }} + shardIndex: ${{ steps.determine-tests.outputs.shardIndex }} + shardTotal: ${{ steps.determine-tests.outputs.shardTotal }} steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: @@ -96,17 +104,16 @@ jobs: - run: npm run build - name: Identify E2E Test Files to run id: determine-tests - run: node ./scripts/ci/find-tests.mjs testsToRun + run: node ./scripts/ci/determine-tests.mjs testsToRun shardIndex shardTotal env: projectRoot: ${{ github.workspace }} - shell: bash - - name: Determine Shard Values - id: set-matrix - run: node ./scripts/ci/determine-shard.mjs shardIndex shardTotal - env: - testsToRun: ${{ steps.determine-tests.outputs.testsToRun }} maximumShards: ${{ env.maximumShards }} shell: bash + - name: Log Shard Values for Debugging + run: | + echo "Shard Index: ${{ steps.determine-tests.outputs.shardIndex }}" + echo "Shard Total: ${{ steps.determine-tests.outputs.shardTotal }}" + shell: bash playwright-atomic: name: 'Run Playwright tests for Atomic' needs: prepare-playwright-atomic @@ -115,8 +122,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: ${{fromJson(needs.prepare-playwright-atomic.outputs.shardIndex || '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]')}} - shardTotal: ${{fromJson(needs.prepare-playwright-atomic.outputs.shardTotal || '[24]')}} + shardIndex: ${{ fromJson(needs.prepare-playwright-atomic.outputs.shardIndex) }} + shardTotal: ${{ fromJson(needs.prepare-playwright-atomic.outputs.shardTotal) }} steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: ./.github/actions/setup diff --git a/.gitignore b/.gitignore index a386022b863..d5be9d2b035 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ sarifs **.cy.ts.mp4 .cspellcache scripts/translation-gpt/temporary.json +packages/headless/docs # CI Release topology.json diff --git a/.nvmrc b/.nvmrc index 593cb75bc5c..fdb2eaaff0c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.16.0 \ No newline at end of file +22.11.0 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 6603d0cf302..d8fb45a0552 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,7 +14,6 @@ utils/**/.storybook/** scripts/deploy/execute-deployment-pipeline.mjs **/staticresources/** packages/atomic-hosted-page/loader/**/* -packages/atomic/loader/**/* packages/atomic/docs/**/* packages/atomic/dist-storybook/**/* packages/atomic/src/components/search/atomic-search-interface/lang/*.json diff --git a/package-lock.json b/package-lock.json index 8761edede7d..11d2ead531d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "packages/atomic-react", "packages/atomic-angular", "packages/atomic-angular/projects/*", - "packages/rollup-plugin-replace-with-ast", "packages/samples/*", "packages/samples/headless-ssr/*", "utils/*" @@ -86,6 +85,7 @@ "patch-package": "8.0.0", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.5", + "publint": "0.2.11", "react-syntax-highlighter": "15.5.0", "rimraf": "5.0.9", "semver": "7.6.3", @@ -95,7 +95,7 @@ "vite": "~5.3.0" }, "engines": { - "node": "^20.9.0", + "node": "^20.9.0 || ^22.11.0", "npm": ">=8.6.0" } }, @@ -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" @@ -5059,9 +5048,19 @@ "resolved": "utils/release", "link": true }, - "node_modules/@coveo/rollup-plugin-replace-with-ast": { - "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", @@ -10885,27 +10884,6 @@ } } }, - "node_modules/@rollup/plugin-replace": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", - "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", @@ -11483,6 +11461,82 @@ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, + "node_modules/@shikijs/core": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.21.0.tgz", + "integrity": "sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.21.0", + "@shikijs/engine-oniguruma": "1.21.0", + "@shikijs/types": "1.21.0", + "@shikijs/vscode-textmate": "^9.2.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/core/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.21.0.tgz", + "integrity": "sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.21.0", + "@shikijs/vscode-textmate": "^9.2.2", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.21.0.tgz", + "integrity": "sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.21.0", + "@shikijs/vscode-textmate": "^9.2.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.21.0.tgz", + "integrity": "sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.2.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/types/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz", + "integrity": "sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -20635,6 +20689,16 @@ "@types/mdurl": "^2" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", @@ -23687,6 +23751,7 @@ "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -23710,6 +23775,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -23718,6 +23784,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -23728,12 +23795,14 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/body-parser/node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, "dependencies": { "side-channel": "^1.0.4" }, @@ -24297,6 +24366,17 @@ "node": ">= 10" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", @@ -24419,6 +24499,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -24623,10 +24714,11 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -25842,38 +25934,6 @@ "typescript": ">=4" } }, - "node_modules/coveo.analytics": { - "version": "2.30.39", - "resolved": "https://registry.npmjs.org/coveo.analytics/-/coveo.analytics-2.30.39.tgz", - "integrity": "sha512-Vce17Mq9lwoBY587ZHqLOcKCu0ufymrWpiZ1X0K6NeoDFygcBFz/7vkpn+mnihG/67AnylhGVcloDWkfRLqnEQ==", - "license": "MIT", - "dependencies": { - "@types/uuid": "^9.0.0", - "cross-fetch": "^3.1.5", - "react-native-get-random-values": "^1.11.0", - "uuid": "^9.0.0" - } - }, - "node_modules/coveo.analytics/node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/coveo.analytics/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -27925,6 +27985,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -30205,30 +30279,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-jest-dom": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.4.0.tgz", - "integrity": "sha512-yBqvFsnpS5Sybjoq61cJiUsenRkC9K32hYQBFS9doBR7nbQZZ5FyO+X7MlmfM1C48Ejx/qTuOCgukDUNyzKZ7A==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.16.3", - "requireindex": "^1.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", - "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "@testing-library/dom": { - "optional": true - } - } - }, "node_modules/eslint-plugin-json-es": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-json-es/-/eslint-plugin-json-es-1.5.4.tgz", @@ -31184,6 +31234,7 @@ "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -31225,6 +31276,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -31232,12 +31284,14 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/express/node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, "dependencies": { "side-channel": "^1.0.4" }, @@ -33688,6 +33742,80 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/hast-util-to-html/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-html/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-html/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-to-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", @@ -33710,6 +33838,30 @@ "@types/unist": "*" } }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/hastscript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", @@ -34016,6 +34168,17 @@ "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", "dev": true }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/html-webpack-plugin": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", @@ -40276,6 +40439,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lws": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lws/-/lws-4.2.0.tgz", @@ -40749,6 +40919,38 @@ "integrity": "sha512-V5Jw1rIdjt37vfQRqvKtXW4dKbSTpvgwyEPKOBikY90xQ5Wr5yOmfpjcTm12d0Kqq+TfMqlXJkETf4yOF9JhUw==", "dev": true }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -40808,7 +41010,8 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true }, "node_modules/merge-stream": { "version": "2.0.0", @@ -40833,6 +41036,100 @@ "node": ">= 0.6" } }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -43459,6 +43756,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/only": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", @@ -44334,7 +44644,8 @@ "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -45717,6 +46028,26 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, + "node_modules/publint": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.2.11.tgz", + "integrity": "sha512-/kxbd+sD/uEG515N/ZYpC6gYs8h89cQ4UIsAq1y6VT4qlNh8xmiSwcP2xU2MbzXFl8J0l2IdONKFweLfYoqhcA==", + "dev": true, + "dependencies": { + "npm-packlist": "^5.1.3", + "picocolors": "^1.1.0", + "sade": "^1.8.1" + }, + "bin": { + "publint": "lib/cli.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -46587,6 +46918,13 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true, + "license": "MIT" + }, "node_modules/regex-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", @@ -46849,15 +47187,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true, - "engines": { - "node": ">=0.10.5" - } - }, "node_modules/requirejs": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", @@ -47852,6 +48181,7 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -47875,6 +48205,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -47882,12 +48213,14 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "bin": { "mime": "cli.js" }, @@ -47898,7 +48231,8 @@ "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/sentence-case": { "version": "3.0.4", @@ -48205,6 +48539,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -48456,6 +48791,31 @@ "node": "*" } }, + "node_modules/shiki": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.21.0.tgz", + "integrity": "sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.21.0", + "@shikijs/engine-javascript": "1.21.0", + "@shikijs/engine-oniguruma": "1.21.0", + "@shikijs/types": "1.21.0", + "@shikijs/vscode-textmate": "^9.2.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/shiki/node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -49588,6 +49948,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -50496,6 +50882,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" } @@ -50580,6 +50967,17 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -51355,6 +51753,48 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "dev": true }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/unist-util-visit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", @@ -51739,6 +52179,50 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", @@ -53522,13 +54006,24 @@ "tslib": "^2.3.0" } }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "packages/atomic": { "name": "@coveo/atomic", - "version": "3.4.0", + "version": "3.7.0", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/store": "2.0.16", @@ -53548,7 +54043,6 @@ "@babel/core": "7.24.9", "@coveo/atomic-storybook-utils": "file:./storybookUtils", "@coveo/release": "1.0.0", - "@coveo/rollup-plugin-replace-with-ast": "1.0.0", "@custom-elements-manifest/analyzer": "0.10.3", "@fullhuman/postcss-purgecss": "6.0.0", "@nx/js": "19.5.3", @@ -53623,11 +54117,11 @@ "wait-on": "7.2.0" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0" + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0" } }, "packages/atomic-angular": { @@ -53642,14 +54136,14 @@ "@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.7.0", "rxjs": "7.8.1" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.9", "@angular/cli": "17.3.9", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "@types/node": "20.14.12", "jasmine-core": "5.2.0", "karma": "6.4.3", @@ -53662,10 +54156,10 @@ "typescript": "5.4.5" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" } }, "packages/atomic-angular/node_modules/jasmine-core": { @@ -53694,28 +54188,28 @@ }, "packages/atomic-angular/projects/atomic-angular": { "name": "@coveo/atomic-angular", - "version": "3.1.6", + "version": "3.2.0", "license": "Apache-2.0", "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.7.0", "tslib": "2.6.3" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" } }, "packages/atomic-hosted-page": { "name": "@coveo/atomic-hosted-page", - "version": "1.0.7", + "version": "1.0.8", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "@stencil/core": "4.20.0" }, "devDependencies": { @@ -53724,7 +54218,7 @@ "@types/node": "20.14.12" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } }, "packages/atomic-hosted-page/node_modules/@playwright/test": { @@ -53797,17 +54291,14 @@ }, "packages/atomic-react": { "name": "@coveo/atomic-react", - "version": "3.1.6", + "version": "3.2.0", "dependencies": { - "@coveo/atomic": "3.4.0" + "@coveo/atomic": "3.7.0" }, "devDependencies": { "@coveo/release": "1.0.0", - "@coveo/rollup-plugin-replace-with-ast": "1.0.0", "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-replace": "^5.0.0", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "^11.0.0", "@types/node": "20.14.12", @@ -53821,10 +54312,10 @@ "rollup-plugin-polyfill-node": "^0.13.0" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } @@ -56555,7 +57046,7 @@ "vite": "5.3.5" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } }, "packages/auth/node_modules/@esbuild/aix-ppc64": { @@ -57145,7 +57636,7 @@ }, "packages/bueno": { "name": "@coveo/bueno", - "version": "1.0.1", + "version": "1.0.2", "license": "Apache-2.0", "devDependencies": { "@coveo/release": "1.0.0", @@ -57153,7 +57644,7 @@ "ts-jest": "29.2.3" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } }, "packages/bueno/node_modules/ts-jest": { @@ -57206,16 +57697,16 @@ }, "packages/headless": { "name": "@coveo/headless", - "version": "3.4.0", + "version": "3.5.0", "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "1.0.1", + "@coveo/bueno": "1.0.2", "@coveo/relay": "0.7.10", "@coveo/relay-event-types": "12.0.1", "@microsoft/fetch-event-source": "2.0.1", "@reduxjs/toolkit": "2.2.7", "abortcontroller-polyfill": "1.7.5", - "coveo.analytics": "2.30.39", + "coveo.analytics": "2.30.42", "dayjs": "1.11.12", "exponential-backoff": "3.1.0", "fast-equals": "5.0.1", @@ -57235,10 +57726,11 @@ "execa": "8.0.1", "install": "0.13.0", "ts-node": "10.9.2", + "typedoc": "0.26.10", "vitest": "2.1.1" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { "encoding": "^0.1.13", @@ -57247,30 +57739,27 @@ }, "packages/headless-react": { "name": "@coveo/headless-react", - "version": "2.0.7", + "version": "2.0.8", "license": "Apache-2.0", "dependencies": { - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" }, "devDependencies": { "@coveo/release": "1.0.0", "@testing-library/react": "14.3.1", - "@types/jest": "29.5.12", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.17.0", - "eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-react": "7.35.0", "eslint-plugin-testing-library": "6.2.2", "gts": "5.3.1", - "jest-environment-jsdom": "29.7.0", "publint": "0.2.9", "rimraf": "5.0.9", "typescript": "5.4.5", "vitest": "2.1.1" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "peerDependencies": { "react": "^18", @@ -57901,6 +58390,27 @@ "node": ">= 16" } }, + "packages/headless/node_modules/coveo.analytics": { + "version": "2.30.42", + "resolved": "https://registry.npmjs.org/coveo.analytics/-/coveo.analytics-2.30.42.tgz", + "integrity": "sha512-O+S7tCJIypYRF4aqsJc6KZtUC4c7oQE5WhrMSgieJT4jSlOWwasfKCSQhhN+YJ0XgLw5kQRqxiBZFHcCU5jX5A==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^9.0.0", + "cross-fetch": "^3.1.5", + "react-native-get-random-values": "^1.11.0", + "uuid": "^9.0.0" + } + }, + "packages/headless/node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "packages/headless/node_modules/dayjs": { "version": "1.11.12", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", @@ -57932,6 +58442,19 @@ "node": ">=6" } }, + "packages/headless/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "packages/headless/node_modules/exponential-backoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.0.tgz", @@ -57960,6 +58483,16 @@ "graceful-fs": "^4.1.6" } }, + "packages/headless/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "packages/headless/node_modules/loupe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", @@ -57990,6 +58523,38 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "packages/headless/node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "packages/headless/node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "packages/headless/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "packages/headless/node_modules/minimatch": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", @@ -58066,6 +58631,45 @@ "node": ">=14.0.0" } }, + "packages/headless/node_modules/typedoc": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.7.tgz", + "integrity": "sha512-gUeI/Wk99vjXXMi8kanwzyhmeFEGv1LTdTQsiyIsmSYsBebvFxhbcyAx7Zjo4cMbpLGxM4Uz3jVIjksu/I2v6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.16.2", + "yaml": "^2.5.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" + } + }, + "packages/headless/node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/headless/node_modules/typescript": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", @@ -58079,6 +58683,13 @@ "node": ">=14.17" } }, + "packages/headless/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "packages/headless/node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -58088,6 +58699,19 @@ "node": ">= 4.0.0" } }, + "packages/headless/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "packages/headless/node_modules/vite-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", @@ -58195,14 +58819,27 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "packages/headless/node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "packages/quantic": { "name": "@coveo/quantic", - "version": "3.2.1", + "version": "3.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "dompurify": "3.1.6", "marked": "12.0.2" }, @@ -58240,7 +58877,7 @@ "xml2js": "0.6.2" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } }, "packages/quantic/node_modules/@babel/compat-data": { @@ -59884,6 +60521,7 @@ "packages/rollup-plugin-replace-with-ast": { "name": "@coveo/rollup-plugin-replace-with-ast", "version": "1.0.0", + "extraneous": true, "license": "Apache-2.0", "devDependencies": { "@rollup/plugin-typescript": "11.1.6", @@ -59905,225 +60543,6 @@ } } }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.0.0-24.tgz", - "integrity": "sha512-19cF3V1fHfzPzwu0cgZEdWLMdNkqSmKOhidqQv1CkUqAMcb7etA7WLx8YrX5ob31ruI0BYYrUDBunlIuMHHUrg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-android-arm64": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.0.0-24.tgz", - "integrity": "sha512-ftTp5ByyyozDsHfmYGeErrQmBi4ZEVZItC4Siilwretkf+cMv9z0s0Ru8ncd28OZpaO0cr9b7Afm+DIRDyE8Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.0.0-24.tgz", - "integrity": "sha512-wYXWdPbMLiIRHQeTF/r9ZoDcf3k1ROR0Kyd/caUtbs5VEZOBfnpZ/FHQPzXW0S1fzxTtD5W4tXULxARMHAlNdQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.0.0-24.tgz", - "integrity": "sha512-8tIz6Uga/5XdeRkid7kfNtxrvru7o4lDBxAPooZezKXbyB2ap2yKAKCqTFEXyTuPhl2yxLMa5zqZ91FBEnSbPg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.0.0-24.tgz", - "integrity": "sha512-ZCNBOaw2NV3BnpQ049VCPJSamss3wAoCunFcWYfhWgGyu9C0hiRvZAcKvhd7e/9EhuoIxsNxMLwI46NmZx9WBQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.0.0-24.tgz", - "integrity": "sha512-BGnRktAZq4RI6FSicI+F6ws9paiYmjyaXUNKSukLthzgzPC91V4SXVylbFOCKvrhdWAr0lvZgcTrkgYNAmAcuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.0.0-24.tgz", - "integrity": "sha512-FzhHpp+vRTjIUYXMExj9Ffj2bCQgnRAzlWlsQTdYGYvPQMVadfPMvnlcr4Li8P7Yv1iBFtDzRVfZAgL5glvIAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.0.0-24.tgz", - "integrity": "sha512-0y+oXnCCT5+U5V58bY7dy65yDrWWfopFJwtC2EbFeA9SHrVjG36/TQo535ML3zdFwO+fma8r5FP1os0psbQBXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.0.0-24.tgz", - "integrity": "sha512-xT8djNMxVB84cCm1XILpQXkMcu8S+GBKjurXM4sc6eB1FQpFpcTAOsuQSg9xOhfPqm1xa7qqXA6ZpUhoUMboVQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.0.0-24.tgz", - "integrity": "sha512-3gXFBlG5f18xbhVxKTM+zwciJPk097i3YswLI9cajVd4MAqMw5bGbuZkGOZOMnkzeIX0ELxovYWPbGDyUr+f5g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.0.0-24.tgz", - "integrity": "sha512-41+QkzRaKEZwmA14Fa2DI0QKN5hkcN/orA2KOg5vJAtvwSfB1uQTUmf6T4SGZLw/8In2TEmViB9tDVlbnXmH1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/rollup-plugin-replace-with-ast/node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "packages/rollup-plugin-replace-with-ast/node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "packages/rollup-plugin-replace-with-ast/node_modules/rollup": { - "version": "4.0.0-24", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.0.0-24.tgz", - "integrity": "sha512-Tcdk9cYyF5abnUQP68AWuSHahowglrzQH6olnHB4Lxi7VBuflwrlpavK7d046Ep2WmwDN0ey5sr+QzLShQ7Odw==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.0.0-24", - "@rollup/rollup-android-arm64": "4.0.0-24", - "@rollup/rollup-darwin-arm64": "4.0.0-24", - "@rollup/rollup-darwin-x64": "4.0.0-24", - "@rollup/rollup-linux-arm-gnueabihf": "4.0.0-24", - "@rollup/rollup-linux-arm64-gnu": "4.0.0-24", - "@rollup/rollup-linux-x64-gnu": "4.0.0-24", - "@rollup/rollup-linux-x64-musl": "4.0.0-24", - "@rollup/rollup-win32-arm64-msvc": "4.0.0-24", - "@rollup/rollup-win32-ia32-msvc": "4.0.0-24", - "@rollup/rollup-win32-x64-msvc": "4.0.0-24", - "fsevents": "~2.3.2" - } - }, - "packages/rollup-plugin-replace-with-ast/node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "packages/samples/angular": { "name": "@coveo/atomic-angular-samples", "version": "0.0.0", @@ -60136,7 +60555,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.2.0", "rxjs": "7.8.1", "tslib": "2.6.3", "zone.js": "0.14.8" @@ -60436,9 +60855,9 @@ "name": "@coveo/atomic-next-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/atomic-react": "3.2.0", + "@coveo/headless": "3.5.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" @@ -60501,9 +60920,9 @@ "name": "@coveo/atomic-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/atomic-react": "3.2.0", + "@coveo/headless": "3.5.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -60991,7 +61410,7 @@ "name": "@coveo/headless-commerce-react-samples", "version": "0.1.0", "dependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -62284,7 +62703,7 @@ "version": "0.0.0", "dependencies": { "@coveo/auth": "2.0.1", - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", @@ -62296,7 +62715,7 @@ "@types/react-router-dom": "5.3.3", "dayjs": "1.11.12", "escape-html": "1.0.3", - "express": "4.19.2", + "express": "4.20.0", "filesize": "10.1.4", "react": "18.3.1", "react-dom": "18.3.1", @@ -62315,7 +62734,7 @@ "vitest": "2.1.1" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } }, "packages/samples/headless-react/node_modules/@adobe/css-tools": { @@ -63546,6 +63965,60 @@ "node": ">=12" } }, + "packages/samples/headless-react/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "packages/samples/headless-react/node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "packages/samples/headless-react/node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "packages/samples/headless-react/node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "packages/samples/headless-react/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -63755,6 +64228,15 @@ "dev": true, "license": "ISC" }, + "packages/samples/headless-react/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "packages/samples/headless-react/node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -63817,6 +64299,63 @@ "url": "https://opencollective.com/eslint" } }, + "packages/samples/headless-react/node_modules/express": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "packages/samples/headless-react/node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "packages/samples/headless-react/node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "packages/samples/headless-react/node_modules/filesize": { "version": "10.1.4", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.4.tgz", @@ -63859,6 +64398,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/samples/headless-react/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "packages/samples/headless-react/node_modules/is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", @@ -63900,6 +64451,27 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "packages/samples/headless-react/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/samples/headless-react/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "packages/samples/headless-react/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -63980,6 +64552,12 @@ "node": ">=4" } }, + "packages/samples/headless-react/node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, "packages/samples/headless-react/node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -64019,6 +64597,21 @@ "node": "^10 || ^12 || >=14" } }, + "packages/samples/headless-react/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "packages/samples/headless-react/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -64031,6 +64624,129 @@ "rimraf": "bin.js" } }, + "packages/samples/headless-react/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "packages/samples/headless-react/node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "packages/samples/headless-react/node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "packages/samples/headless-react/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/samples/headless-react/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "packages/samples/headless-react/node_modules/serve-static": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "packages/samples/headless-react/node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "packages/samples/headless-react/node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "packages/samples/headless-react/node_modules/serve-static/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "packages/samples/headless-react/node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "packages/samples/headless-react/node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "packages/samples/headless-react/node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -64370,8 +65086,8 @@ "name": "@coveo/headless-ssr-samples-common", "version": "0.0.0", "dependencies": { - "@coveo/headless": "3.4.0", - "@coveo/headless-react": "2.0.7", + "@coveo/headless": "3.5.0", + "@coveo/headless-react": "2.0.8", "next": "14.2.5", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -64392,7 +65108,7 @@ "name": "@coveo/headless-ssr-commerce-samples", "version": "0.0.0", "dependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "next": "14.2.5", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -64511,10 +65227,10 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "3.4.0", - "@coveo/atomic-hosted-page": "1.0.7", - "@coveo/atomic-react": "3.1.6", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/atomic-hosted-page": "1.0.8", + "@coveo/atomic-react": "3.2.0", + "@coveo/headless": "3.5.0", "react": "18.3.1", "react-dom": "18.3.1" }, @@ -64583,9 +65299,9 @@ "name": "@coveo/atomic-stencil-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "@stencil/core": "4.20.0", "stencil-router-v2": "0.6.0" }, @@ -64868,7 +65584,7 @@ "name": "@coveo/atomic-vuejs-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.7.0", "vue": "^3.4.15" }, "devDependencies": { @@ -65478,7 +66194,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", @@ -65495,20 +66211,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/package.json b/package.json index 1bfadb68ef5..1c05e34af7c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "e2e": "nx run-many --target=e2e", "lint:check": "eslint . && cspell **/*.md --no-progress --show-suggestions --show-context && prettier --check '**/*.{css,pcss,html,md,yml,ts,tsx,js,mjs,json}'", "lint:fix": "eslint --fix . && prettier --write '**/*.{css,pcss,html,md,yml,ts,tsx,js,mjs,json}'", + "package-compatibility": "node ./scripts/ci/package-compatibility.mjs", "pr:report": "node ./scripts/reports/pr-report.mjs", "report:bundle-size:time-series": "node ./scripts/reports/bundle-size/time-series.mjs", "commit": "git-cz", @@ -83,6 +84,7 @@ "patch-package": "8.0.0", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.5", + "publint": "0.2.11", "react-syntax-highlighter": "15.5.0", "rimraf": "5.0.9", "semver": "7.6.3", @@ -110,7 +112,6 @@ "packages/atomic-react", "packages/atomic-angular", "packages/atomic-angular/projects/*", - "packages/rollup-plugin-replace-with-ast", "packages/samples/*", "packages/samples/headless-ssr/*", "utils/*" @@ -133,7 +134,7 @@ ] }, "engines": { - "node": "^20.9.0", + "node": "^20.9.0 || ^22.11.0", "npm": ">=8.6.0" }, "dependencies": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index a0c6584a746..c3605fbf4c9 100644 --- a/packages/atomic-angular/package.json +++ b/packages/atomic-angular/package.json @@ -20,17 +20,17 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.7.0", "rxjs": "7.8.1" }, "peerDependencies": { - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" }, "devDependencies": { "@angular-devkit/build-angular": "17.3.9", "@angular/cli": "17.3.9", "@angular/compiler-cli": "17.3.12", - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "@types/node": "20.14.12", "jasmine-core": "5.2.0", "karma": "6.4.3", @@ -43,6 +43,6 @@ "typescript": "5.4.5" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } } diff --git a/packages/atomic-angular/projects/atomic-angular/CHANGELOG.md b/packages/atomic-angular/projects/atomic-angular/CHANGELOG.md index 8f76c184370..9a368f12700 100644 --- a/packages/atomic-angular/projects/atomic-angular/CHANGELOG.md +++ b/packages/atomic-angular/projects/atomic-angular/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.2.0 (2024-10-30) + +- feat(atomic): support highlights in atomic-product-description (#4541) ([5c235a8](https://github.com/coveo/ui-kit/commits/5c235a8)), closes [#4541](https://github.com/coveo/ui-kit/issues/4541) +- fix(atomic-angular): add @Prop decorator to @MapProp props so they are generated in the angular-outp ([dc2faaf](https://github.com/coveo/ui-kit/commits/dc2faaf)), closes [#4548](https://github.com/coveo/ui-kit/issues/4548) +- chore: promote the v3 branch when publishing on v3 (#4585) ([7b9144d](https://github.com/coveo/ui-kit/commits/7b9144d)), closes [#4585](https://github.com/coveo/ui-kit/issues/4585) [#4584](https://github.com/coveo/ui-kit/issues/4584) + ## 3.1.0 (2024-09-24) - feat(atomic): add tab support for atomic-generated-answer (#4285) ([744fb61](https://github.com/coveo/ui-kit/commits/744fb61)), closes [#4285](https://github.com/coveo/ui-kit/issues/4285) diff --git a/packages/atomic-angular/projects/atomic-angular/package.json b/packages/atomic-angular/projects/atomic-angular/package.json index 7ccbf6fbd09..0047eca8632 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.2.0", "license": "Apache-2.0", "repository": { "url": "https://github.com/coveo/ui-kit" @@ -8,13 +8,13 @@ "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" }, "dependencies": { - "@coveo/atomic": "3.4.0", + "@coveo/atomic": "3.7.0", "tslib": "2.6.3" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } } 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-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts index f523722e45a..9b316b4cbbf 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/atomic-angular.module.ts @@ -62,9 +62,11 @@ AtomicPopover, AtomicProduct, AtomicProductChildren, AtomicProductDescription, +AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, +AtomicProductMultiValueText, AtomicProductNumericFieldValue, AtomicProductPrice, AtomicProductRating, @@ -205,9 +207,11 @@ AtomicPopover, AtomicProduct, AtomicProductChildren, AtomicProductDescription, +AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, +AtomicProductMultiValueText, AtomicProductNumericFieldValue, AtomicProductPrice, AtomicProductRating, diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts index d57c6c8a5c9..cf1daa76a9e 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts @@ -873,14 +873,14 @@ export declare interface AtomicFacetManager extends Components.AtomicFacetManage @ProxyCmp({ - inputs: ['ifDefined', 'ifNotDefined'] + inputs: ['ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'] }) @Component({ selector: 'atomic-field-condition', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['ifDefined', 'ifNotDefined'], + inputs: ['ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'], }) export class AtomicFieldCondition { protected el: HTMLElement; @@ -1278,14 +1278,14 @@ export declare interface AtomicProductChildren extends Components.AtomicProductC @ProxyCmp({ - inputs: ['field', 'truncateAfter'] + inputs: ['field', 'isCollapsible', 'truncateAfter'] }) @Component({ selector: 'atomic-product-description', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['field', 'truncateAfter'], + inputs: ['field', 'isCollapsible', 'truncateAfter'], }) export class AtomicProductDescription { protected el: HTMLElement; @@ -1300,14 +1300,36 @@ export declare interface AtomicProductDescription extends Components.AtomicProdu @ProxyCmp({ - inputs: ['ifDefined', 'ifNotDefined'] + inputs: ['isCollapsible', 'truncateAfter'] +}) +@Component({ + selector: 'atomic-product-excerpt', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['isCollapsible', 'truncateAfter'], +}) +export class AtomicProductExcerpt { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface AtomicProductExcerpt extends Components.AtomicProductExcerpt {} + + +@ProxyCmp({ + inputs: ['ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'] }) @Component({ selector: 'atomic-product-field-condition', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['ifDefined', 'ifNotDefined'], + inputs: ['ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'], }) export class AtomicProductFieldCondition { protected el: HTMLElement; @@ -1366,6 +1388,28 @@ export class AtomicProductLink { export declare interface AtomicProductLink extends Components.AtomicProductLink {} +@ProxyCmp({ + inputs: ['delimiter', 'field', 'maxValuesToDisplay'] +}) +@Component({ + selector: 'atomic-product-multi-value-text', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['delimiter', 'field', 'maxValuesToDisplay'], +}) +export class AtomicProductMultiValueText { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface AtomicProductMultiValueText extends Components.AtomicProductMultiValueText {} + + @ProxyCmp({ inputs: ['field'] }) @@ -1871,14 +1915,14 @@ export declare interface AtomicRecsList extends Components.AtomicRecsList {} @ProxyCmp({ - inputs: ['classes', 'content', 'density', 'display', 'imageSize', 'result', 'stopPropagation'] + inputs: ['classes', 'content', 'density', 'display', 'imageSize', 'linkContent', 'result', 'stopPropagation'] }) @Component({ selector: 'atomic-recs-result', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['classes', 'content', 'density', 'display', 'imageSize', 'result', 'stopPropagation'], + inputs: ['classes', 'content', 'density', 'display', 'imageSize', 'linkContent', 'result', 'stopPropagation'], }) export class AtomicRecsResult { protected el: HTMLElement; @@ -1893,7 +1937,7 @@ export declare interface AtomicRecsResult extends Components.AtomicRecsResult {} @ProxyCmp({ - inputs: ['conditions', 'ifDefined', 'ifNotDefined'], + inputs: ['conditions', 'ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'], methods: ['getTemplate'] }) @Component({ @@ -1901,7 +1945,7 @@ export declare interface AtomicRecsResult extends Components.AtomicRecsResult {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['conditions', 'ifDefined', 'ifNotDefined'], + inputs: ['conditions', 'ifDefined', 'ifNotDefined', 'mustMatch', 'mustNotMatch'], }) export class AtomicRecsResultTemplate { protected el: HTMLElement; @@ -2052,7 +2096,7 @@ export declare interface AtomicResultChildren extends Components.AtomicResultChi @ProxyCmp({ - inputs: ['conditions'], + inputs: ['conditions', 'mustMatch', 'mustNotMatch'], methods: ['getTemplate'] }) @Component({ @@ -2060,7 +2104,7 @@ export declare interface AtomicResultChildren extends Components.AtomicResultChi changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['conditions'], + inputs: ['conditions', 'mustMatch', 'mustNotMatch'], }) export class AtomicResultChildrenTemplate { protected el: HTMLElement; @@ -2228,14 +2272,14 @@ export declare interface AtomicResultList extends Components.AtomicResultList {} @ProxyCmp({ - inputs: ['fieldCount', 'localeKey'] + inputs: ['field', 'fieldCount', 'localeKey'] }) @Component({ selector: 'atomic-result-localized-text', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['fieldCount', 'localeKey'], + inputs: ['field', 'fieldCount', 'localeKey'], }) export class AtomicResultLocalizedText { protected el: HTMLElement; @@ -2528,7 +2572,7 @@ export declare interface AtomicResultSectionVisual extends Components.AtomicResu @ProxyCmp({ - inputs: ['conditions'], + inputs: ['conditions', 'mustMatch', 'mustNotMatch'], methods: ['getTemplate'] }) @Component({ @@ -2536,7 +2580,7 @@ export declare interface AtomicResultSectionVisual extends Components.AtomicResu changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['conditions'], + inputs: ['conditions', 'mustMatch', 'mustNotMatch'], }) export class AtomicResultTemplate { protected el: HTMLElement; diff --git a/packages/atomic-hosted-page/CHANGELOG.md b/packages/atomic-hosted-page/CHANGELOG.md index 7701408c6aa..49048312eba 100644 --- a/packages/atomic-hosted-page/CHANGELOG.md +++ b/packages/atomic-hosted-page/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 (2024-10-30) + +- chore: promote the v3 branch when publishing on v3 (#4585) ([7b9144d](https://github.com/coveo/ui-kit/commits/7b9144d)), closes [#4585](https://github.com/coveo/ui-kit/issues/4585) [#4584](https://github.com/coveo/ui-kit/issues/4584) + ## 1.0.0 (2024-09-18) - chore!: update node engine definition in all exported packages (#4330) ([d6d8a1a](https://github.com/coveo/ui-kit/commits/d6d8a1a)), closes [#4330](https://github.com/coveo/ui-kit/issues/4330) diff --git a/packages/atomic-hosted-page/package.json b/packages/atomic-hosted-page/package.json index f8b5b44a613..ff355b9f651 100644 --- a/packages/atomic-hosted-page/package.json +++ b/packages/atomic-hosted-page/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic-hosted-page", "description": "Web Component used to inject a Coveo Hosted Search Page in the DOM.", - "version": "1.0.7", + "version": "1.0.8", "repository": { "type": "git", "url": "git+https://github.com/coveo/ui-kit.git", @@ -27,11 +27,11 @@ "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", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "@stencil/core": "4.20.0" }, "devDependencies": { @@ -40,6 +40,6 @@ "@types/node": "20.14.12" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } } diff --git a/packages/atomic-react/CHANGELOG.md b/packages/atomic-react/CHANGELOG.md index ec24eb5e323..322b54ee588 100644 --- a/packages/atomic-react/CHANGELOG.md +++ b/packages/atomic-react/CHANGELOG.md @@ -1,3 +1,16 @@ +## 3.2.0 (2024-10-30) + +- fix(atomic-react): remove traces of npm imports (#4581) ([599b36f](https://github.com/coveo/ui-kit/commits/599b36f)), closes [#4581](https://github.com/coveo/ui-kit/issues/4581) [/github.com/coveo/ui-kit/pull/4581/files#r1812922022](https://github.com//github.com/coveo/ui-kit/pull/4581/files/issues/r1812922022) +- fix(atomic-react): render empty link container when display is other than grid (#4604) ([91a2538](https://github.com/coveo/ui-kit/commits/91a2538)), closes [#4604](https://github.com/coveo/ui-kit/issues/4604) +- fix(atomic): allow atomic loader to be deployed to the CDN (#4568) ([b579cb0](https://github.com/coveo/ui-kit/commits/b579cb0)), closes [#4568](https://github.com/coveo/ui-kit/issues/4568) +- feat(atomic): support highlights in atomic-product-description (#4541) ([5c235a8](https://github.com/coveo/ui-kit/commits/5c235a8)), closes [#4541](https://github.com/coveo/ui-kit/issues/4541) +- chore: promote the v3 branch when publishing on v3 (#4585) ([7b9144d](https://github.com/coveo/ui-kit/commits/7b9144d)), closes [#4585](https://github.com/coveo/ui-kit/issues/4585) [#4584](https://github.com/coveo/ui-kit/issues/4584) +- chore: remove rollup-plugin-replace-with-ast package (#4591) ([34cd096](https://github.com/coveo/ui-kit/commits/34cd096)), closes [#4591](https://github.com/coveo/ui-kit/issues/4591) + +## 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..724c2ce930d 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.2.0", "description": "React specific wrapper for the Atomic component library", "repository": { "type": "git", @@ -12,33 +12,30 @@ "scripts": { "build": "nx build", "clean": "rimraf -rf dist", + "build:fixLoaderImportPaths": "node ./scripts/fix-loader-import-paths.js", "build:fixGeneratedImportPaths": "fix-esm-import-path src/components/stencil-generated", - "build:bundles:esm": "tsc -p tsconfig.esm.json", - "build:bundles:iife-cjs": "rollup --config rollup.config.mjs --bundleConfigAsCjs", - "build:bundles": "concurrently \"npm run build:bundles:esm\" \"npm run build:bundles:iife-cjs\"", + "build:bundles": "rollup --config rollup.config.js", + "build:types": "tsc --project tsconfig.types.json", "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 " }, + "module": "./dist/esm/atomic-react.mjs", "main": "./dist/cjs/atomic-react.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", + "types": "./dist/types/index.d.ts", "files": [ "dist/", "recommendation/", "commerce/" ], "dependencies": { - "@coveo/atomic": "3.4.0" + "@coveo/atomic": "3.7.0" }, "devDependencies": { "@coveo/release": "1.0.0", - "@coveo/rollup-plugin-replace-with-ast": "1.0.0", "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-replace": "^5.0.0", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "^11.0.0", "@types/node": "20.14.12", @@ -52,27 +49,27 @@ "rollup-plugin-polyfill-node": "^0.13.0" }, "peerDependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/atomic-react.mjs", "require": "./dist/cjs/atomic-react.cjs" }, "./commerce": { - "types": "./dist/commerce.index.d.ts", - "import": "./dist/commerce.index.js", + "types": "./dist/types/commerce.index.d.ts", + "import": "./dist/esm/commerce/atomic-react.mjs", "require": "./dist/cjs/commerce/atomic-react.cjs" }, "./recommendation": { - "types": "./dist/recommendation.index.d.ts", - "import": "./dist/recommendation.index.js", + "types": "./dist/types/recommendation.index.d.ts", + "import": "./dist/esm/recommendation/atomic-react.mjs", "require": "./dist/cjs/recommendation/atomic-react.cjs" } } diff --git a/packages/atomic-react/project.json b/packages/atomic-react/project.json index 5af3ade9955..12d21937f6f 100644 --- a/packages/atomic-react/project.json +++ b/packages/atomic-react/project.json @@ -8,8 +8,10 @@ "executor": "nx:run-commands", "options": { "commands": [ + "npm run build:fixLoaderImportPaths", "npm run build:fixGeneratedImportPaths", "npm run build:bundles", + "npm run build:types", "npm run build:assets" ], "parallel": false, diff --git a/packages/atomic-react/rollup.config.js b/packages/atomic-react/rollup.config.js new file mode 100644 index 00000000000..90ce545748a --- /dev/null +++ b/packages/atomic-react/rollup.config.js @@ -0,0 +1,167 @@ +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import {readFileSync} from 'fs'; +import {join, dirname} from 'path'; +import {defineConfig} from 'rollup'; +import nodePolyfills from 'rollup-plugin-polyfill-node'; +import {fileURLToPath} from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const isCDN = process.env.DEPLOYMENT_ENVIRONMENT === 'CDN'; + +let headlessVersion; +let atomicVersion; + +if (isCDN) { + console.log('Building for CDN'); + + const headlessPackageJsonPath = join( + __dirname, + '../../packages/headless/package.json' + ); + const atomicPackageJsonPath = join( + __dirname, + '../../packages/atomic/package.json' + ); + + try { + const headlessPackageJson = JSON.parse( + readFileSync(headlessPackageJsonPath, 'utf8') + ); + headlessVersion = 'v' + headlessPackageJson.version; + console.log('Using headless version from package.json:', headlessVersion); + + const atomicPackageJson = JSON.parse( + readFileSync(atomicPackageJsonPath, 'utf8') + ); + atomicVersion = 'v' + atomicPackageJson.version; + console.log('Using atomic version from package.json:', atomicVersion); + } catch (error) { + console.error('Error reading headless package.json:', error); + throw new Error('Error reading headless package.json'); + } +} + +const packageMappings = { + '@coveo/headless/commerce': { + cdn: `/headless/${headlessVersion}/commerce/headless.esm.js`, + }, + '@coveo/headless/insight': { + cdn: `/headless/${headlessVersion}/insight/headless.esm.js`, + }, + '@coveo/headless/recommendation': { + cdn: `/headless/${headlessVersion}/recommendation/headless.esm.js`, + }, + '@coveo/headless/case-assist': { + cdn: `/headless/${headlessVersion}/case-assist/headless.esm.js`, + }, + '@coveo/headless': { + cdn: `/headless/${headlessVersion}/headless.esm.js`, + }, + '@coveo/atomic/loader': { + cdn: `/atomic/${atomicVersion}/loader/index.js`, + }, +}; + +const externalizeDependenciesPlugin = () => { + return { + name: 'externalize-dependencies', + resolveId: (source, _importer, _options) => { + const packageMapping = packageMappings[source]; + + if (packageMapping) { + console.log(`Package cdn import : ${packageMapping.cdn}`); + + return { + id: packageMapping.cdn, + external: 'absolute', + }; + } + + return null; + }, + }; +}; + +/** @type {import('rollup').ExternalOption} */ +const commonExternal = [ + 'react', + 'react-dom', + 'react-dom/client', + 'react-dom/server', + '@coveo/atomic/loader', + '@coveo/headless', + '@coveo/headless/recommendation', + '@coveo/headless/commerce', +]; + +/** @type {import('rollup').ExternalOption} */ +const cdnExternal = [ + 'react', + 'react-dom', + 'react-dom/client', + 'react-dom/server', +]; + +/** @returns {import('rollup').OutputOptions} */ +const outputCJS = ({useCase}) => ({ + file: `dist/cjs/${useCase}atomic-react.cjs`, + format: 'cjs', + sourcemap: true, +}); + +/** @returns {import('rollup').OutputOptions} */ +const outputESM = ({useCase}) => ({ + file: `dist/esm/${useCase}atomic-react.mjs`, + format: 'esm', + sourcemap: true, +}); + +/**@type {import('rollup').InputPluginOption} */ +const plugins = [ + nodePolyfills(), + typescript(), + nodeResolve(), + isCDN && externalizeDependenciesPlugin(), +]; + +export default defineConfig([ + { + input: 'src/index.ts', + output: [outputCJS({useCase: ''})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, + { + input: 'src/index.ts', + output: [outputESM({useCase: ''})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, + { + input: 'src/recommendation.index.ts', + output: [outputCJS({useCase: 'recommendation/'})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, + { + input: 'src/recommendation.index.ts', + output: [outputESM({useCase: 'recommendation/'})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, + { + input: 'src/commerce.index.ts', + output: [outputCJS({useCase: 'commerce/'})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, + { + input: 'src/commerce.index.ts', + output: [outputESM({useCase: 'commerce/'})], + external: isCDN ? cdnExternal : commonExternal, + plugins: plugins, + }, +]); diff --git a/packages/atomic-react/rollup.config.mjs b/packages/atomic-react/rollup.config.mjs deleted file mode 100644 index cc4e9596f95..00000000000 --- a/packages/atomic-react/rollup.config.mjs +++ /dev/null @@ -1,197 +0,0 @@ -import replaceWithASTPlugin from '@coveo/rollup-plugin-replace-with-ast'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import {nodeResolve} from '@rollup/plugin-node-resolve'; -import replace from '@rollup/plugin-replace'; -// import terser from '@rollup/plugin-terser'; -import typescript from '@rollup/plugin-typescript'; -import {readFileSync} from 'fs'; -import {join, dirname} from 'path'; -import {defineConfig} from 'rollup'; -import nodePolyfills from 'rollup-plugin-polyfill-node'; -import {fileURLToPath} from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const isCDN = process.env.DEPLOYMENT_ENVIRONMENT === 'CDN'; - -let headlessVersion; - -if (isCDN) { - console.log('Building for CDN'); - - const headlessPackageJsonPath = join( - __dirname, - '../../packages/headless/package.json' - ); - - try { - const headlessPackageJson = JSON.parse( - readFileSync(headlessPackageJsonPath, 'utf8') - ); - headlessVersion = 'v' + headlessPackageJson.version; - console.log('Using headless version from package.json:', headlessVersion); - } catch (error) { - console.error('Error reading headless package.json:', error); - throw new Error('Error reading headless package.json'); - } -} - -function generateReplaceValues() { - return Object.entries(packageMappings).reduce((acc, [find, paths]) => { - acc[find] = paths.cdn; - return acc; - }, {}); -} - -const packageMappings = { - '@coveo/headless/commerce': { - cdn: `/headless/${headlessVersion}/commerce/headless.esm.js`, - }, - '@coveo/headless/insight': { - cdn: `/headless/${headlessVersion}/insight/headless.esm.js`, - }, - '@coveo/headless/recommendation': { - cdn: `/headless/${headlessVersion}/recommendation/headless.esm.js`, - }, - '@coveo/headless/case-assist': { - cdn: `/headless/${headlessVersion}/case-assist/headless.esm.js`, - }, - '@coveo/headless': { - cdn: `/headless/${headlessVersion}/headless.esm.js`, - }, -}; - -// /** @type {import("rollup").GlobalsOption} */ -// const globals = { -// react: 'React', -// 'react-dom': 'ReactDOM', -// 'react-dom/client': 'ReactDOM', -// 'react-dom/server': 'ReactDOMServer', -// '@coveo/atomic': 'CoveoAtomic', -// '@coveo/headless': 'CoveoHeadless', -// }; - -/** @type {import('rollup').ExternalOption} */ -const commonExternal = [ - 'react', - 'react-dom', - 'react-dom/client', - 'react-dom/server', - '@coveo/atomic', - '@coveo/headless', -]; - -// /** @returns {import('rollup').OutputOptions} */ -// const outputIIFE = ({minify}) => ({ -// file: `dist/iife/atomic-react${minify ? '.min' : ''}.js`, -// format: 'iife', -// name: 'CoveoAtomicReact', -// globals, -// plugins: minify ? [terser()] : [], -// }); - -/** @returns {import('rollup').OutputOptions} */ -const outputCJS = ({useCase}) => ({ - file: `dist/cjs/${useCase}atomic-react.cjs`, - format: 'cjs', -}); - -// /** @returns {import('rollup').OutputOptions} */ -// const outputIIFERecs = ({minify}) => ({ -// file: `dist/iife/atomic-react/recommendation${minify ? '.min' : ''}.js`, -// format: 'iife', -// name: 'CoveoAtomicReactRecommendation', -// globals, -// plugins: minify ? [terser()] : [], -// }); - -// /** @returns {import('rollup').OutputOptions} */ -// const outputIIFECommerce = ({minify}) => ({ -// file: `dist/iife/atomic-react/commerce${minify ? '.min' : ''}.js`, -// format: 'iife', -// name: 'CoveoAtomicReactCommerce', -// globals, -// plugins: minify ? [terser()] : [], -// }); - -const plugins = [ - isCDN && - replaceWithASTPlugin({ - replacements: generateReplaceValues(), - }), - json(), - nodePolyfills(), - typescript({tsconfig: 'tsconfig.iife.json'}), - commonjs(), - nodeResolve(), - replace({ - delimiters: ['', ''], - values: { - 'process.env.NODE_ENV': JSON.stringify('dev'), - 'util.TextEncoder();': 'TextEncoder();', - "import { defineCustomElements } from '@coveo/atomic/loader';": '', - 'defineCustomElements();': '', - }, - }), -]; - -const pluginsCJS = [ - json(), - nodePolyfills(), - typescript(), - commonjs(), - nodeResolve(), - replace({ - delimiters: ['', ''], - values: { - 'process.env.NODE_ENV': JSON.stringify('dev'), - 'util.TextEncoder();': 'TextEncoder();', - "import { defineCustomElements } from '@coveo/atomic/loader';": '', - 'defineCustomElements();': '', - }, - }), -]; - -export default defineConfig([ - // { - // input: 'src/index.ts', - // output: [outputIIFE({minify: true}), outputIIFE({minify: false})], - // external: commonExternal, - // plugins, - // }, - { - input: 'src/index.ts', - output: [outputCJS({useCase: ''})], - external: commonExternal, - plugins: pluginsCJS, - }, - // { - // input: 'src/recommendation.index.ts', - // output: [outputIIFERecs({minify: true}), outputIIFERecs({minify: false})], - // external: commonExternal, - // plugins, - // }, - { - input: 'src/recommendation.index.ts', - output: [outputCJS({useCase: 'recommendation/'})], - external: commonExternal, - plugins: pluginsCJS, - }, - // { - // input: 'src/commerce.index.ts', - // output: [ - // outputIIFECommerce({minify: true}), - // outputIIFECommerce({minify: false}), - // ], - // external: commonExternal, - // plugins, - // }, - { - input: 'src/commerce.index.ts', - output: [outputCJS({useCase: 'commerce/'})], - external: commonExternal, - plugins: pluginsCJS, - }, -]); diff --git a/packages/atomic-react/scripts/fix-loader-import-paths.js b/packages/atomic-react/scripts/fix-loader-import-paths.js new file mode 100644 index 00000000000..0a8283afaae --- /dev/null +++ b/packages/atomic-react/scripts/fix-loader-import-paths.js @@ -0,0 +1,29 @@ +import {promises as fs} from 'fs'; +import path from 'path'; + +const files = [ + path.resolve('src/components/stencil-generated/commerce/index.ts'), + path.resolve('src/components/stencil-generated/search/index.ts'), +]; + +const oldImport = + "import { defineCustomElements } from '@coveo/atomic/dist/loader';"; +const newImport = + "import { defineCustomElements } from '@coveo/atomic/loader';"; + +const updateFiles = async () => { + await Promise.all( + files.map(async (filePath) => { + try { + let data = await fs.readFile(filePath, 'utf8'); + const updatedData = data.replace(oldImport, newImport); + await fs.writeFile(filePath, updatedData, 'utf8'); + console.log(`File updated: ${filePath}`); + } catch (err) { + console.error(`Error updating file: ${filePath}`, err); + } + }) + ); +}; + +updateFiles(); diff --git a/packages/atomic-react/src/components/commerce/CommerceProductListWrapper.tsx b/packages/atomic-react/src/components/commerce/CommerceProductListWrapper.tsx index b8cd37bac9a..bb7d1eb5049 100644 --- a/packages/atomic-react/src/components/commerce/CommerceProductListWrapper.tsx +++ b/packages/atomic-react/src/components/commerce/CommerceProductListWrapper.tsx @@ -44,9 +44,11 @@ export const ListWrapper: React.FC = (props) => { return renderToString(templateResult.contentTemplate); } else { createRoot(root).render(templateResult); - createRoot(linkContainer!).render( - - ); + otherProps.display === 'grid' + ? createRoot(linkContainer!).render( + + ) + : createRoot(linkContainer!).render(<>); return renderToString(templateResult); } } diff --git a/packages/atomic-react/src/components/commerce/CommerceRecommendationListWrapper.tsx b/packages/atomic-react/src/components/commerce/CommerceRecommendationListWrapper.tsx index ee54bd59c5a..6f12df20eb6 100644 --- a/packages/atomic-react/src/components/commerce/CommerceRecommendationListWrapper.tsx +++ b/packages/atomic-react/src/components/commerce/CommerceRecommendationListWrapper.tsx @@ -44,9 +44,11 @@ export const ListWrapper: React.FC = (props) => { return renderToString(templateResult.contentTemplate); } else { createRoot(root).render(templateResult); - createRoot(linkContainer!).render( - - ); + otherProps.display === 'grid' + ? createRoot(linkContainer!).render( + + ) + : createRoot(linkContainer!).render(<>); return renderToString(templateResult); } } diff --git a/packages/atomic-react/src/components/recommendation/RecsListWrapper.tsx b/packages/atomic-react/src/components/recommendation/RecsListWrapper.tsx index 1cca9001acd..8cf85c843de 100644 --- a/packages/atomic-react/src/components/recommendation/RecsListWrapper.tsx +++ b/packages/atomic-react/src/components/recommendation/RecsListWrapper.tsx @@ -42,9 +42,11 @@ export const RecsListWrapper: React.FC = (props) => { return renderToString(templateResult.contentTemplate); } else { createRoot(root).render(templateResult); - createRoot(linkContainer!).render( - - ); + otherProps.display === 'grid' + ? createRoot(linkContainer!).render( + + ) + : createRoot(linkContainer!).render(<>); return renderToString(templateResult); } }); diff --git a/packages/atomic-react/src/components/search/ResultListWrapper.tsx b/packages/atomic-react/src/components/search/ResultListWrapper.tsx index a678a38ada4..0ddcc7f82ce 100644 --- a/packages/atomic-react/src/components/search/ResultListWrapper.tsx +++ b/packages/atomic-react/src/components/search/ResultListWrapper.tsx @@ -42,9 +42,11 @@ export const ResultListWrapper: React.FC = (props) => { return renderToString(templateResult.contentTemplate); } else { createRoot(root).render(templateResult); - createRoot(linkContainer!).render( - - ); + otherProps.display === 'grid' + ? createRoot(linkContainer!).render( + + ) + : createRoot(linkContainer!).render(<>); return renderToString(templateResult); } }); diff --git a/packages/atomic-react/src/components/stencil-generated/commerce/index.ts b/packages/atomic-react/src/components/stencil-generated/commerce/index.ts index 1d1213dc90c..9c6d44ac100 100644 --- a/packages/atomic-react/src/components/stencil-generated/commerce/index.ts +++ b/packages/atomic-react/src/components/stencil-generated/commerce/index.ts @@ -41,9 +41,11 @@ export const AtomicNumericRange = /*@__PURE__*/createReactComponent('atomic-product'); export const AtomicProductChildren = /*@__PURE__*/createReactComponent('atomic-product-children'); export const AtomicProductDescription = /*@__PURE__*/createReactComponent('atomic-product-description'); +export const AtomicProductExcerpt = /*@__PURE__*/createReactComponent('atomic-product-excerpt'); export const AtomicProductFieldCondition = /*@__PURE__*/createReactComponent('atomic-product-field-condition'); export const AtomicProductImage = /*@__PURE__*/createReactComponent('atomic-product-image'); export const AtomicProductLink = /*@__PURE__*/createReactComponent('atomic-product-link'); +export const AtomicProductMultiValueText = /*@__PURE__*/createReactComponent('atomic-product-multi-value-text'); export const AtomicProductNumericFieldValue = /*@__PURE__*/createReactComponent('atomic-product-numeric-field-value'); export const AtomicProductPrice = /*@__PURE__*/createReactComponent('atomic-product-price'); export const AtomicProductRating = /*@__PURE__*/createReactComponent('atomic-product-rating'); diff --git a/packages/atomic-react/tsconfig.iife.json b/packages/atomic-react/tsconfig.iife.json deleted file mode 100644 index b3ebd30b24e..00000000000 --- a/packages/atomic-react/tsconfig.iife.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "module": "es2020", - "outDir": "dist/iife" - } -} diff --git a/packages/atomic-react/tsconfig.json b/packages/atomic-react/tsconfig.json index d715e10e72c..e25f19a5390 100644 --- a/packages/atomic-react/tsconfig.json +++ b/packages/atomic-react/tsconfig.json @@ -1,25 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "allowUnreachableCode": false, - "allowSyntheticDefaultImports": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "esModuleInterop": true, "lib": ["dom", "ES2023"], "moduleResolution": "Bundler", "module": "ES2022", - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "removeComments": false, - "sourceMap": true, "jsx": "react", "target": "ES2022" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["**/__tests__/**"], - "compileOnSave": false, - "buildOnSave": false + "include": ["src/**/*.ts", "src/**/*.tsx"] } diff --git a/packages/atomic-react/tsconfig.esm.json b/packages/atomic-react/tsconfig.types.json similarity index 59% rename from packages/atomic-react/tsconfig.esm.json rename to packages/atomic-react/tsconfig.types.json index 04ac54f50fd..1f68f1f2435 100644 --- a/packages/atomic-react/tsconfig.esm.json +++ b/packages/atomic-react/tsconfig.types.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, - "module": "es2020", - "outDir": "dist" + "emitDeclarationOnly": true, + "outDir": "dist/types" } } diff --git a/packages/atomic/.eslintrc.cjs b/packages/atomic/.eslintrc.cjs index 5078021b491..3226298d692 100644 --- a/packages/atomic/.eslintrc.cjs +++ b/packages/atomic/.eslintrc.cjs @@ -13,7 +13,6 @@ module.exports = { 'src/external-builds/**/*', 'dist/**/*', 'www/**/*', - 'loader/**/*', 'docs/**/*', 'dist-storybook', ], diff --git a/packages/atomic/CHANGELOG.md b/packages/atomic/CHANGELOG.md index 3d8c2882dcd..2c1e7e7d07d 100644 --- a/packages/atomic/CHANGELOG.md +++ b/packages/atomic/CHANGELOG.md @@ -1,3 +1,28 @@ +## 3.7.0 (2024-10-30) + +- feat(atomic): support highlights in atomic-product-description (#4541) ([5c235a8](https://github.com/coveo/ui-kit/commits/5c235a8)), closes [#4541](https://github.com/coveo/ui-kit/issues/4541) +- chore: promote the v3 branch when publishing on v3 (#4585) ([7b9144d](https://github.com/coveo/ui-kit/commits/7b9144d)), closes [#4585](https://github.com/coveo/ui-kit/issues/4585) [#4584](https://github.com/coveo/ui-kit/issues/4584) +- chore: remove rollup-plugin-replace-with-ast package (#4591) ([34cd096](https://github.com/coveo/ui-kit/commits/34cd096)), closes [#4591](https://github.com/coveo/ui-kit/issues/4591) +- ci: reenable tests disabled in #4294 (#4306) ([ebf55db](https://github.com/coveo/ui-kit/commits/ebf55db)), closes [#4294](https://github.com/coveo/ui-kit/issues/4294) [#4306](https://github.com/coveo/ui-kit/issues/4306) +- feat(atomic, headless): leverage totalNumberOfChildResults in folded results (#4513) ([9dc3af1](https://github.com/coveo/ui-kit/commits/9dc3af1)), closes [#4513](https://github.com/coveo/ui-kit/issues/4513) +- feat(atomic,headless): add Quickview Support for Insight (#4479) ([95ac6a8](https://github.com/coveo/ui-kit/commits/95ac6a8)), closes [#4479](https://github.com/coveo/ui-kit/issues/4479) +- fix(headless,commerce): clear manual ranges on clearAllCoreFacets (#4593) ([208cd63](https://github.com/coveo/ui-kit/commits/208cd63)), closes [#4593](https://github.com/coveo/ui-kit/issues/4593) +- fix(atomic-angular): add @Prop decorator to @MapProp props so they are generated in the angular-outp ([dc2faaf](https://github.com/coveo/ui-kit/commits/dc2faaf)), closes [#4548](https://github.com/coveo/ui-kit/issues/4548) +- fix(atomic-numeric-facet storybook): storybook bug fix for numeric facet (#4565) ([2dbad78](https://github.com/coveo/ui-kit/commits/2dbad78)), closes [#4565](https://github.com/coveo/ui-kit/issues/4565) +- fix(atomic): allow atomic loader to be deployed to the CDN (#4568) ([b579cb0](https://github.com/coveo/ui-kit/commits/b579cb0)), closes [#4568](https://github.com/coveo/ui-kit/issues/4568) + +## 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/cypress/e2e/facets/color-facet/color-facet.cypress.ts b/packages/atomic/cypress/e2e/facets/color-facet/color-facet.cypress.ts index 6aa03d46f8c..d591913c47e 100644 --- a/packages/atomic/cypress/e2e/facets/color-facet/color-facet.cypress.ts +++ b/packages/atomic/cypress/e2e/facets/color-facet/color-facet.cypress.ts @@ -61,65 +61,6 @@ describe('Color Facet Test Suites', () => { CommonFacetAssertions.assertDisplaySearchInput(ColorFacetSelectors, true); }); - describe('when searching for a value that returns results', () => { - const query = 'html'; - function setupSearchFor() { - typeFacetSearchQuery(ColorFacetSelectors, query, true); - } - - beforeEach(setupSearchFor); - describe('verify rendering', () => { - CommonAssertions.assertAccessibility(colorFacetComponent); - ColorFacetAssertions.assertNumberOfIdleBoxValues(1); - CommonFacetAssertions.assertDisplaySearchClearButton( - ColorFacetSelectors, - true - ); - CommonFacetAssertions.assertHighlightsResults( - ColorFacetSelectors, - query - ); - }); - - describe('when selecting a search result', () => { - function setupSelectSearchResult() { - AnalyticsTracker.reset(); - selectIdleBoxValueAt(0); - } - - beforeEach(setupSelectSearchResult); - describe('verify rendering', () => { - ColorFacetAssertions.assertNumberOfSelectedBoxValues(1); - ColorFacetAssertions.assertNumberOfIdleBoxValues( - colorFacetDefaultNumberOfValues - 1 - ); - CommonFacetAssertions.assertSearchInputEmpty(ColorFacetSelectors); - }); - - describe('verify analytics', () => { - it('should log the facet select results to UA ', () => { - cy.expectSearchEvent('facetSelect').then((analyticsBody) => { - expect(analyticsBody.customData).to.have.property( - 'facetField', - colorFacetField - ); - expect(analyticsBody.facetState[0]).to.have.property( - 'state', - 'selected' - ); - expect(analyticsBody.facetState[0]).to.have.property( - 'field', - colorFacetField - ); - expect(analyticsBody.customData).to.have.property( - 'facetValue', - query - ); - }); - }); - }); - }); - }); describe('when selecting a value', () => { const selectionIndex = 1; function setupSelectBoxValue() { @@ -194,6 +135,66 @@ describe('Color Facet Test Suites', () => { }); }); }); + + describe('when searching for a value that returns results', () => { + const query = 'doc'; + function setupSearchFor() { + typeFacetSearchQuery(ColorFacetSelectors, query, true); + } + + beforeEach(setupSearchFor); + describe('verify rendering', () => { + CommonAssertions.assertAccessibility(colorFacetComponent); + ColorFacetAssertions.assertNumberOfIdleBoxValues(1); + CommonFacetAssertions.assertDisplaySearchClearButton( + ColorFacetSelectors, + true + ); + CommonFacetAssertions.assertHighlightsResults( + ColorFacetSelectors, + query + ); + }); + + describe('when selecting a search result', () => { + function setupSelectSearchResult() { + AnalyticsTracker.reset(); + selectIdleBoxValueAt(0); + } + + beforeEach(setupSelectSearchResult); + describe('verify rendering', () => { + ColorFacetAssertions.assertNumberOfSelectedBoxValues(2); + ColorFacetAssertions.assertNumberOfIdleBoxValues( + colorFacetDefaultNumberOfValues - 2 + ); + CommonFacetAssertions.assertSearchInputEmpty(ColorFacetSelectors); + }); + + describe('verify analytics', () => { + it('should log the facet select results to UA ', () => { + cy.expectSearchEvent('facetSelect').then((analyticsBody) => { + expect(analyticsBody.customData).to.have.property( + 'facetField', + colorFacetField + ); + expect(analyticsBody.facetState[0]).to.have.property( + 'state', + 'selected' + ); + expect(analyticsBody.facetState[0]).to.have.property( + 'field', + colorFacetField + ); + expect(analyticsBody.customData).to.have.property( + 'facetValue', + query + ); + }); + }); + }); + }); + }); }); }); @@ -222,7 +223,7 @@ describe('Color Facet Test Suites', () => { ColorFacetSelectors, true ); - // CommonAssertions.assertAccessibility(colorFacetComponent); + CommonAssertions.assertAccessibility(colorFacetComponent); ColorFacetAssertions.assertValuesSortedAlphanumerically(); ColorFacetAssertions.assertNumberOfIdleBoxValues( colorFacetDefaultNumberOfValues * 2 diff --git a/packages/atomic/cypress/e2e/facets/rating-facet/rating-facet.cypress.ts b/packages/atomic/cypress/e2e/facets/rating-facet/rating-facet.cypress.ts index fe73fd5c793..3661e71e90b 100644 --- a/packages/atomic/cypress/e2e/facets/rating-facet/rating-facet.cypress.ts +++ b/packages/atomic/cypress/e2e/facets/rating-facet/rating-facet.cypress.ts @@ -378,10 +378,10 @@ describe('Rating Facet Test Suites', () => { RatingFacetSelectors, ratingFacetDefaultNumberOfIntervals - 1 ); - // RatingFacetAssertions.assertSelectedFacetValueContainsNumberOfStar( - // RatingFacetSelectors, - // 4 - // ); + RatingFacetAssertions.assertSelectedFacetValueContainsNumberOfStar( + RatingFacetSelectors, + 4 + ); }); describe('with depends-on', () => { diff --git a/packages/atomic/cypress/e2e/facets/rating-range-facet/rating-range-facet.cypress.ts b/packages/atomic/cypress/e2e/facets/rating-range-facet/rating-range-facet.cypress.ts index 4a01789b321..0cea0bc6cfe 100644 --- a/packages/atomic/cypress/e2e/facets/rating-range-facet/rating-range-facet.cypress.ts +++ b/packages/atomic/cypress/e2e/facets/rating-range-facet/rating-range-facet.cypress.ts @@ -180,48 +180,47 @@ describe('Rating Range Test Suites', () => { RatingRangeFacetAssertions.assertFacetValueContainsAndUp(); }); - // describe('with custom #maxValueInIndex', () => { - // const customMaxValueInIndex = 3; - // function setupRatingFacetWithCustomMaxValueInIndex() { - // new TestFixture() - // .with( - // addRatingRangeFacet({ - // field: ratingRangeFacetField, - // label: ratingRangeFacetLabel, - // 'number-of-intervals': '5', - // 'max-value-in-index': customMaxValueInIndex, - // }) - // ) - // .init(); - // } + describe('with custom #maxValueInIndex', () => { + const customMaxValueInIndex = 4; + function setupRatingFacetWithCustomMaxValueInIndex() { + new TestFixture() + .with( + addRatingRangeFacet({ + field: ratingRangeFacetField, + label: ratingRangeFacetLabel, + 'max-value-in-index': customMaxValueInIndex, + }) + ) + .init(); + } - // beforeEach(setupRatingFacetWithCustomMaxValueInIndex); - // CommonAssertions.assertAccessibility(ratingRangeFacetComponent); - // CommonAssertions.assertContainsComponentError( - // RatingRangeFacetSelectors, - // false - // ); - // CommonFacetAssertions.assertDisplayFacet(RatingRangeFacetSelectors, true); - // CommonFacetAssertions.assertNumberOfSelectedLinkValues( - // RatingRangeFacetSelectors, - // 0 - // ); - // CommonFacetAssertions.assertNumberOfIdleLinkValues( - // RatingRangeFacetSelectors, - // ratingRangeFacetDefaultNumberOfIntervals - // ); - // RatingFacetAssertions.assertNumberOfStarAtIndex( - // RatingRangeFacetSelectors, - // customMaxValueInIndex - // ); - // RatingFacetAssertions.assertNumberofYellowStar( - // RatingRangeFacetSelectors, - // 0, - // 4, - // customMaxValueInIndex - // ); - // RatingRangeFacetAssertions.assertFacetValueContainsTextOnlyAndUp(); - // }); + beforeEach(setupRatingFacetWithCustomMaxValueInIndex); + CommonAssertions.assertAccessibility(ratingRangeFacetComponent); + CommonAssertions.assertContainsComponentError( + RatingRangeFacetSelectors, + false + ); + CommonFacetAssertions.assertDisplayFacet(RatingRangeFacetSelectors, true); + CommonFacetAssertions.assertNumberOfSelectedLinkValues( + RatingRangeFacetSelectors, + 0 + ); + CommonFacetAssertions.assertNumberOfIdleLinkValues( + RatingRangeFacetSelectors, + ratingRangeFacetDefaultNumberOfIntervals + ); + RatingFacetAssertions.assertNumberOfStarAtIndex( + RatingRangeFacetSelectors, + customMaxValueInIndex + ); + RatingFacetAssertions.assertNumberofYellowStar( + RatingRangeFacetSelectors, + 0, + 4, + customMaxValueInIndex + ); + RatingRangeFacetAssertions.assertFacetValueContainsTextOnlyAndUp(); + }); describe('with custom #minValueInIndex', () => { const customMinValueInIndex = 2; @@ -325,10 +324,10 @@ describe('Rating Range Test Suites', () => { RatingRangeFacetSelectors, ratingRangeFacetDefaultNumberOfIntervals - 1 ); - // RatingFacetAssertions.assertSelectedFacetValueContainsNumberOfStar( - // RatingRangeFacetSelectors, - // 4 - // ); + RatingFacetAssertions.assertSelectedFacetValueContainsNumberOfStar( + RatingRangeFacetSelectors, + 4 + ); RatingRangeFacetAssertions.assertFacetValueContainsTextOnlyAndUp(); }); describe('with depends-on', () => { diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 697c058ec44..5a5f21680f1 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.7.0", "description": "A web-component library for building modern UIs interfacing with the Coveo platform", "homepage": "https://docs.coveo.com/en/atomic/latest/", "repository": { @@ -14,9 +14,9 @@ "types": "dist/types/index.d.ts", "exports": { "./loader": { - "types": "./loader/index.d.ts", - "import": "./loader/index.js", - "require": "./loader/index.cjs.js" + "types": "./dist/loader/index.d.ts", + "import": "./dist/loader/index.js", + "require": "./dist/loader/index.cjs.js" }, ".": { "types": "./dist/types/index.d.ts", @@ -39,8 +39,7 @@ "files": [ "dist/", "docs/", - "licenses/", - "loader/" + "licenses/" ], "scripts": { "clean": "rimraf -rf dist/* dist-storybook/* www/* docs/* loader/* playwright-report/*", @@ -62,12 +61,12 @@ "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": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "@popperjs/core": "^2.11.6", "@salesforce-ux/design-system": "^2.16.1", "@stencil/store": "2.0.16", @@ -87,7 +86,6 @@ "@babel/core": "7.24.9", "@coveo/atomic-storybook-utils": "file:./storybookUtils", "@coveo/release": "1.0.0", - "@coveo/rollup-plugin-replace-with-ast": "1.0.0", "@custom-elements-manifest/analyzer": "0.10.3", "@fullhuman/postcss-purgecss": "6.0.0", "@nx/js": "19.5.3", @@ -162,11 +160,11 @@ "wait-on": "7.2.0" }, "peerDependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0" + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0" }, "license": "Apache-2.0", "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" } } diff --git a/packages/atomic/project.json b/packages/atomic/project.json index 6b1ab3fa7a9..e0f4e24627b 100644 --- a/packages/atomic/project.json +++ b/packages/atomic/project.json @@ -152,7 +152,7 @@ "dependsOn": ["^build", "cached:build:stencil"], "executor": "nx:run-commands", "options": { - "command": "rm ./loader/package.json", + "command": "rm ./dist/loader/package.json", "cwd": "packages/atomic" } } diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index 1e258dc0818..a42bf5c8f9d 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -28,6 +28,7 @@ import { InsightResultActionClickedEvent } from "./components/insight/atomic-ins import { Section } from "./components/common/atomic-layout-section/sections"; import { AtomicCommonStore, AtomicCommonStoreData } from "./components/common/interface/store"; import { SelectChildProductEventArgs } from "./components/commerce/product-template-components/atomic-product-children/atomic-product-children"; +import { TruncateAfter } from "./components/common/expandable-text/expandable-text"; import { RecommendationEngine } from "@coveo/headless/recommendation"; import { InteractiveResult as RecsInteractiveResult, LogLevel as RecsLogLevel, Result as RecsResult, ResultTemplate as RecsResultTemplate, ResultTemplateCondition as RecsResultTemplateCondition } from "./components/recommendations"; import { RecsInitializationOptions } from "./components/recommendations/atomic-recs-interface/atomic-recs-interface"; @@ -58,6 +59,7 @@ export { InsightResultActionClickedEvent } from "./components/insight/atomic-ins export { Section } from "./components/common/atomic-layout-section/sections"; export { AtomicCommonStore, AtomicCommonStoreData } from "./components/common/interface/store"; export { SelectChildProductEventArgs } from "./components/commerce/product-template-components/atomic-product-children/atomic-product-children"; +export { TruncateAfter } from "./components/common/expandable-text/expandable-text"; export { RecommendationEngine } from "@coveo/headless/recommendation"; export { InteractiveResult as RecsInteractiveResult, LogLevel as RecsLogLevel, Result as RecsResult, ResultTemplate as RecsResultTemplate, ResultTemplateCondition as RecsResultTemplateCondition } from "./components/recommendations"; export { RecsInitializationOptions } from "./components/recommendations/atomic-recs-interface/atomic-recs-interface"; @@ -946,6 +948,16 @@ export namespace Components { * Verifies whether the specified fields are not defined. */ "ifNotDefined"?: string; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch": Record; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch": Record; } interface AtomicFocusTrap { "active": boolean; @@ -1069,7 +1081,7 @@ export namespace Components { interface AtomicGeneratedAnswer { "answerConfigurationId"?: string; /** - * Whether to allow the answer to be collapsed when the text is taller than 250px. + * Whether to allow the answer to be collapsed when the text is taller than the specified `--atomic-crga-collapsed-height` value (16rem by default). * @default false */ "collapsible"?: boolean; @@ -1444,6 +1456,20 @@ export namespace Components { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch": Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch": Record< + string, + string[] + >; } interface AtomicInsightResultList { /** @@ -1460,6 +1486,12 @@ export namespace Components { */ "setRenderFunction": (resultRenderingFunction: ItemRenderingFunction) => Promise; } + interface AtomicInsightResultQuickviewAction { + /** + * The `sandbox` attribute to apply to the quickview iframe. The quickview is loaded inside an iframe with a [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox) attribute for security reasons. This attribute exists primarily to protect against potential XSS attacks that could originate from the document being displayed. By default, the sandbox attributes are: `allow-popups allow-top-navigation allow-same-origin`. `allow-same-origin` is not optional, and must always be included in the list of allowed capabilities for the component to function properly. + */ + "sandbox": string; + } interface AtomicInsightResultTemplate { /** * A function that must return true on results for the result template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to results whose `title` contains `singapore`: `document.querySelector('#target-template').conditions = [(result) => /singapore/i.test(result.title)];` @@ -1477,6 +1509,20 @@ export namespace Components { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch": Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch": Record< + string, + string[] + >; } interface AtomicInsightSearchBox { /** @@ -2028,10 +2074,27 @@ export namespace Components { * The name of the description field to use. */ "field": 'ec_description' | 'ec_shortdesc'; + /** + * Whether the description should be collapsible after being expanded. + */ + "isCollapsible": boolean; /** * The number of lines after which the product description should be truncated. A value of "none" will disable truncation. */ - "truncateAfter": 'none' | '1' | '2' | '3' | '4'; + "truncateAfter": TruncateAfter; + } + /** + * @alpha The `atomic-product-excerpt` component renders the excerpt of a product generated at query time. + */ + interface AtomicProductExcerpt { + /** + * Whether the excerpt should be collapsible after being expanded. + */ + "isCollapsible": boolean; + /** + * The number of lines after which the product excerpt should be truncated. A value of "none" will disable truncation. + */ + "truncateAfter": TruncateAfter; } /** * The `atomic-product-field-condition` component takes a list of conditions that, if fulfilled, apply the template in which it's defined. @@ -2047,6 +2110,16 @@ export namespace Components { * Verifies whether the specified fields are not defined. */ "ifNotDefined"?: string; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch": Record; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch": Record; } /** * The `atomic-product-image` component renders an image from a product field. @@ -2089,6 +2162,23 @@ export namespace Components { */ "hrefTemplate"?: string; } + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface AtomicProductMultiValueText { + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + "delimiter": string | null; + /** + * The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + "field": string; + /** + * The maximum number of field values to display. If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + "maxValuesToDisplay": number; + } /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -2579,6 +2669,11 @@ export namespace Components { * The InteractiveResult item. */ "interactiveResult": RecsInteractiveResult; + /** + * The result link to use when the result is clicked in a grid layout. + * @default - An `atomic-result-link` without any customization. + */ + "linkContent": ParentNode; "loadingFlag"?: string; /** * Internal function used by atomic-recs-list in advanced setups, which lets you bypass the standard HTML template system. Particularly useful for Atomic React @@ -2621,6 +2716,20 @@ export namespace Components { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch": Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch": Record< + string, + string[] + >; } /** * The `atomic-refine-modal` is automatically created as a child of the `atomic-search-interface` when the `atomic-refine-toggle` is initialized. @@ -2781,6 +2890,22 @@ export namespace Components { * Gets the appropriate result template based on conditions applied. */ "getTemplate": () => Promise | null>; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch": Record< + string, + string[] + >; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch": Record< + string, + string[] + >; } /** * The `atomic-result-date` component renders the value of a date result field. @@ -2896,6 +3021,10 @@ export namespace Components { * @MapProp name: field;attr: field;docs: The field from which to extract the target string and the variable used to map it to the target i18n parameter. For example, the following configuration extracts the value of `author` from a result, and assign it to the i18n parameter `name`: `field-author="name"`;type: Record ;default: {} */ interface AtomicResultLocalizedText { + /** + * The field value to dynamically evaluate. + */ + "field": Record; /** * The numerical field value used to determine whether to use the singular or plural value of a translation. */ @@ -3081,6 +3210,22 @@ export namespace Components { * Gets the appropriate result template based on conditions applied. */ "getTemplate": () => Promise | null>; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch": Record< + string, + string[] + >; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch": Record< + string, + string[] + >; } /** * The `atomic-result-text` component renders the value of a string result field. @@ -4465,6 +4610,12 @@ declare global { prototype: HTMLAtomicInsightResultListElement; new (): HTMLAtomicInsightResultListElement; }; + interface HTMLAtomicInsightResultQuickviewActionElement extends Components.AtomicInsightResultQuickviewAction, HTMLStencilElement { + } + var HTMLAtomicInsightResultQuickviewActionElement: { + prototype: HTMLAtomicInsightResultQuickviewActionElement; + new (): HTMLAtomicInsightResultQuickviewActionElement; + }; interface HTMLAtomicInsightResultTemplateElement extends Components.AtomicInsightResultTemplate, HTMLStencilElement { } var HTMLAtomicInsightResultTemplateElement: { @@ -4799,6 +4950,15 @@ declare global { prototype: HTMLAtomicProductDescriptionElement; new (): HTMLAtomicProductDescriptionElement; }; + /** + * @alpha The `atomic-product-excerpt` component renders the excerpt of a product generated at query time. + */ + interface HTMLAtomicProductExcerptElement extends Components.AtomicProductExcerpt, HTMLStencilElement { + } + var HTMLAtomicProductExcerptElement: { + prototype: HTMLAtomicProductExcerptElement; + new (): HTMLAtomicProductExcerptElement; + }; /** * The `atomic-product-field-condition` component takes a list of conditions that, if fulfilled, apply the template in which it's defined. * The condition properties can be based on any top-level product property of the `product` object, not restricted to fields (e.g., `ec_name`). @@ -4829,6 +4989,15 @@ declare global { prototype: HTMLAtomicProductLinkElement; new (): HTMLAtomicProductLinkElement; }; + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface HTMLAtomicProductMultiValueTextElement extends Components.AtomicProductMultiValueText, HTMLStencilElement { + } + var HTMLAtomicProductMultiValueTextElement: { + prototype: HTMLAtomicProductMultiValueTextElement; + new (): HTMLAtomicProductMultiValueTextElement; + }; /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -5916,6 +6085,7 @@ declare global { "atomic-insight-result-children": HTMLAtomicInsightResultChildrenElement; "atomic-insight-result-children-template": HTMLAtomicInsightResultChildrenTemplateElement; "atomic-insight-result-list": HTMLAtomicInsightResultListElement; + "atomic-insight-result-quickview-action": HTMLAtomicInsightResultQuickviewActionElement; "atomic-insight-result-template": HTMLAtomicInsightResultTemplateElement; "atomic-insight-search-box": HTMLAtomicInsightSearchBoxElement; "atomic-insight-smart-snippet": HTMLAtomicInsightSmartSnippetElement; @@ -5950,9 +6120,11 @@ declare global { "atomic-product": HTMLAtomicProductElement; "atomic-product-children": HTMLAtomicProductChildrenElement; "atomic-product-description": HTMLAtomicProductDescriptionElement; + "atomic-product-excerpt": HTMLAtomicProductExcerptElement; "atomic-product-field-condition": HTMLAtomicProductFieldConditionElement; "atomic-product-image": HTMLAtomicProductImageElement; "atomic-product-link": HTMLAtomicProductLinkElement; + "atomic-product-multi-value-text": HTMLAtomicProductMultiValueTextElement; "atomic-product-numeric-field-value": HTMLAtomicProductNumericFieldValueElement; "atomic-product-price": HTMLAtomicProductPriceElement; "atomic-product-rating": HTMLAtomicProductRatingElement; @@ -6887,6 +7059,16 @@ declare namespace LocalJSX { * Verifies whether the specified fields are not defined. */ "ifNotDefined"?: string; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch"?: Record; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch"?: Record; } interface AtomicFocusTrap { "active"?: boolean; @@ -7006,7 +7188,7 @@ declare namespace LocalJSX { interface AtomicGeneratedAnswer { "answerConfigurationId"?: string; /** - * Whether to allow the answer to be collapsed when the text is taller than 250px. + * Whether to allow the answer to be collapsed when the text is taller than the specified `--atomic-crga-collapsed-height` value (16rem by default). * @default false */ "collapsible"?: boolean; @@ -7364,6 +7546,20 @@ declare namespace LocalJSX { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch"?: Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch"?: Record< + string, + string[] + >; } interface AtomicInsightResultList { /** @@ -7375,6 +7571,12 @@ declare namespace LocalJSX { */ "imageSize"?: ItemDisplayImageSize; } + interface AtomicInsightResultQuickviewAction { + /** + * The `sandbox` attribute to apply to the quickview iframe. The quickview is loaded inside an iframe with a [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox) attribute for security reasons. This attribute exists primarily to protect against potential XSS attacks that could originate from the document being displayed. By default, the sandbox attributes are: `allow-popups allow-top-navigation allow-same-origin`. `allow-same-origin` is not optional, and must always be included in the list of allowed capabilities for the component to function properly. + */ + "sandbox"?: string; + } interface AtomicInsightResultTemplate { /** * A function that must return true on results for the result template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to results whose `title` contains `singapore`: `document.querySelector('#target-template').conditions = [(result) => /singapore/i.test(result.title)];` @@ -7388,6 +7590,20 @@ declare namespace LocalJSX { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch"?: Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch"?: Record< + string, + string[] + >; } interface AtomicInsightSearchBox { /** @@ -7925,10 +8141,27 @@ declare namespace LocalJSX { * The name of the description field to use. */ "field"?: 'ec_description' | 'ec_shortdesc'; + /** + * Whether the description should be collapsible after being expanded. + */ + "isCollapsible"?: boolean; /** * The number of lines after which the product description should be truncated. A value of "none" will disable truncation. */ - "truncateAfter"?: 'none' | '1' | '2' | '3' | '4'; + "truncateAfter"?: TruncateAfter; + } + /** + * @alpha The `atomic-product-excerpt` component renders the excerpt of a product generated at query time. + */ + interface AtomicProductExcerpt { + /** + * Whether the excerpt should be collapsible after being expanded. + */ + "isCollapsible"?: boolean; + /** + * The number of lines after which the product excerpt should be truncated. A value of "none" will disable truncation. + */ + "truncateAfter"?: TruncateAfter; } /** * The `atomic-product-field-condition` component takes a list of conditions that, if fulfilled, apply the template in which it's defined. @@ -7944,6 +8177,16 @@ declare namespace LocalJSX { * Verifies whether the specified fields are not defined. */ "ifNotDefined"?: string; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch"?: Record; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch"?: Record; } /** * The `atomic-product-image` component renders an image from a product field. @@ -7973,6 +8216,23 @@ declare namespace LocalJSX { */ "hrefTemplate"?: string; } + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + interface AtomicProductMultiValueText { + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + "delimiter"?: string | null; + /** + * The field that the component should use. The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + "field": string; + /** + * The maximum number of field values to display. If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + "maxValuesToDisplay"?: number; + } /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. @@ -8435,6 +8695,11 @@ declare namespace LocalJSX { * The InteractiveResult item. */ "interactiveResult": RecsInteractiveResult; + /** + * The result link to use when the result is clicked in a grid layout. + * @default - An `atomic-result-link` without any customization. + */ + "linkContent"?: ParentNode; "loadingFlag"?: string; /** * Internal function used by atomic-recs-list in advanced setups, which lets you bypass the standard HTML template system. Particularly useful for Atomic React @@ -8473,6 +8738,20 @@ declare namespace LocalJSX { * The field that, when defined on a result item, would prevent the template from being applied. For example, a template with the following attribute only applies to result items whose `filetype` and `sourcetype` fields are NOT defined: `if-not-defined="filetype,sourcetype"` */ "ifNotDefined"?: string; + /** + * The field and values that define which result items the condition must be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` + */ + "mustMatch"?: Record< + string, + string[] + >; + /** + * The field and values that define which result items the condition must not be applied to. For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` + */ + "mustNotMatch"?: Record< + string, + string[] + >; } /** * The `atomic-refine-modal` is automatically created as a child of the `atomic-search-interface` when the `atomic-refine-toggle` is initialized. @@ -8630,6 +8909,22 @@ declare namespace LocalJSX { * A function that must return true on results for the result template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to results whose `title` contains `singapore`: `document.querySelector('#target-template').conditions = [(result) => /singapore/i.test(result.title)];` */ "conditions"?: ResultTemplateCondition[]; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch"?: Record< + string, + string[] + >; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch"?: Record< + string, + string[] + >; } /** * The `atomic-result-date` component renders the value of a date result field. @@ -8740,6 +9035,10 @@ declare namespace LocalJSX { * @MapProp name: field;attr: field;docs: The field from which to extract the target string and the variable used to map it to the target i18n parameter. For example, the following configuration extracts the value of `author` from a result, and assign it to the i18n parameter `name`: `field-author="name"`;type: Record ;default: {} */ interface AtomicResultLocalizedText { + /** + * The field value to dynamically evaluate. + */ + "field"?: Record; /** * The numerical field value used to determine whether to use the singular or plural value of a translation. */ @@ -8921,6 +9220,22 @@ declare namespace LocalJSX { * A function that must return true on results for the result template to apply. Set programmatically before initialization, not via attribute. For example, the following targets a template and sets a condition to make it apply only to results whose `title` contains `singapore`: `document.querySelector('#target-template').conditions = [(result) => /singapore/i.test(result.title)];` */ "conditions"?: ResultTemplateCondition[]; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + "mustMatch"?: Record< + string, + string[] + >; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + "mustNotMatch"?: Record< + string, + string[] + >; } /** * The `atomic-result-text` component renders the value of a string result field. @@ -9551,6 +9866,7 @@ declare namespace LocalJSX { "atomic-insight-result-children": AtomicInsightResultChildren; "atomic-insight-result-children-template": AtomicInsightResultChildrenTemplate; "atomic-insight-result-list": AtomicInsightResultList; + "atomic-insight-result-quickview-action": AtomicInsightResultQuickviewAction; "atomic-insight-result-template": AtomicInsightResultTemplate; "atomic-insight-search-box": AtomicInsightSearchBox; "atomic-insight-smart-snippet": AtomicInsightSmartSnippet; @@ -9585,9 +9901,11 @@ declare namespace LocalJSX { "atomic-product": AtomicProduct; "atomic-product-children": AtomicProductChildren; "atomic-product-description": AtomicProductDescription; + "atomic-product-excerpt": AtomicProductExcerpt; "atomic-product-field-condition": AtomicProductFieldCondition; "atomic-product-image": AtomicProductImage; "atomic-product-link": AtomicProductLink; + "atomic-product-multi-value-text": AtomicProductMultiValueText; "atomic-product-numeric-field-value": AtomicProductNumericFieldValue; "atomic-product-price": AtomicProductPrice; "atomic-product-rating": AtomicProductRating; @@ -9947,6 +10265,7 @@ declare module "@stencil/core" { "atomic-insight-result-children": LocalJSX.AtomicInsightResultChildren & JSXBase.HTMLAttributes; "atomic-insight-result-children-template": LocalJSX.AtomicInsightResultChildrenTemplate & JSXBase.HTMLAttributes; "atomic-insight-result-list": LocalJSX.AtomicInsightResultList & JSXBase.HTMLAttributes; + "atomic-insight-result-quickview-action": LocalJSX.AtomicInsightResultQuickviewAction & JSXBase.HTMLAttributes; "atomic-insight-result-template": LocalJSX.AtomicInsightResultTemplate & JSXBase.HTMLAttributes; "atomic-insight-search-box": LocalJSX.AtomicInsightSearchBox & JSXBase.HTMLAttributes; "atomic-insight-smart-snippet": LocalJSX.AtomicInsightSmartSnippet & JSXBase.HTMLAttributes; @@ -10034,6 +10353,10 @@ declare module "@stencil/core" { * @alpha The `atomic-product-description` component renders the description of a product. */ "atomic-product-description": LocalJSX.AtomicProductDescription & JSXBase.HTMLAttributes; + /** + * @alpha The `atomic-product-excerpt` component renders the excerpt of a product generated at query time. + */ + "atomic-product-excerpt": LocalJSX.AtomicProductExcerpt & JSXBase.HTMLAttributes; /** * The `atomic-product-field-condition` component takes a list of conditions that, if fulfilled, apply the template in which it's defined. * The condition properties can be based on any top-level product property of the `product` object, not restricted to fields (e.g., `ec_name`). @@ -10049,6 +10372,10 @@ declare module "@stencil/core" { * @alpha The `atomic-product-link` component automatically transforms a search product title into a clickable link that points to the original item. */ "atomic-product-link": LocalJSX.AtomicProductLink & JSXBase.HTMLAttributes; + /** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + */ + "atomic-product-multi-value-text": LocalJSX.AtomicProductMultiValueText & JSXBase.HTMLAttributes; /** * @alpha The `atomic-product-numeric-field-value` component renders the value of a number product field. * The number can be formatted by adding a `atomic-format-number`, `atomic-format-currency` or `atomic-format-unit` component into this component. diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx index 04b03cddb95..a3261de2e8d 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/atomic-commerce-breadbox.tsx @@ -15,6 +15,7 @@ import { Context, ContextState, buildContext, + LocationFacetValue, } from '@coveo/headless/commerce'; import {Component, h, State, Element, Prop} from '@stencil/core'; import {FocusTargetController} from '../../../utils/accessibility-utils'; @@ -42,6 +43,7 @@ import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-int type AnyFacetValue = | RegularFacetValue + | LocationFacetValue | NumericFacetValue | DateFacetValue | CategoryFacetValue; @@ -288,7 +290,7 @@ export class AtomicCommerceBreadbox (pathValue: string) => getFieldValueCaption(field, pathValue, this.bindings.i18n) ); - default: + case 'regular': return [ getFieldValueCaption( field, @@ -296,6 +298,11 @@ export class AtomicCommerceBreadbox this.bindings.i18n ), ]; + default: { + // TODO COMHUB-291 support location breadcrumb + this.bindings.engine.logger.warn('Unexpected breadcrumb type.'); + return []; + } } }; diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts index e30d5eee9d3..adc3d26e6e9 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/atomic-commerce-breadbox.e2e.ts @@ -59,6 +59,23 @@ test.describe('Default', () => { }); }); + test('when restoring a manual numerical range from URL, should show the corresponding breadcrumb', async ({ + page, + breadbox, + }) => { + const baseUrl = + 'http://localhost:4400/iframe.html?args=&id=atomic-commerce-breadbox--default&viewMode=story#sortCriteria=relevance&mnf-ec_price=20..30'; + await page.goto(baseUrl); + + const expectedBreadcrumbLabel = 'Price:$20.00 to $30.00'; + + const breadcrumbButton = breadbox.getBreadcrumbButtons( + expectedBreadcrumbLabel + ); + + await expect(breadcrumbButton).toHaveText(expectedBreadcrumbLabel); + }); + test.describe('when a regular facet value is selected', () => { let firstValueText: string | RegExp; @@ -83,6 +100,20 @@ test.describe('Default', () => { await expect(breadcrumbButton).not.toBeVisible(); }); + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + test('should contain the selected value and the facet name in the breadcrumb button', async ({ breadbox, }) => { @@ -115,6 +146,20 @@ test.describe('Default', () => { await expect(breadcrumbButton).not.toBeVisible(); }); + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + test('should contain the selected value and the facet name in the breadcrumb button', async ({ breadbox, }) => { @@ -156,6 +201,20 @@ test.describe('Default', () => { await expect(breadcrumbButton).not.toBeVisible(); }); + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + test('should contain the selected value and the facet name in the breadcrumb button', async ({ breadbox, }) => { @@ -189,6 +248,63 @@ test.describe('Default', () => { await expect(breadcrumbButton).not.toBeVisible(); }); + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should contain the selected value and the facet name in the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + + await expect(breadcrumbButton).toHaveText('Price:' + firstValueText); + }); + }); + + test.describe('when a manual numerical facet range is applied', () => { + let firstValueText: string | RegExp; + + test.beforeEach(async ({breadbox}) => { + await breadbox.applyManualNumericalRange(20, 30); + firstValueText = '$20.00 to $30.00'; + await breadbox + .getBreadcrumbButtons(firstValueText) + .waitFor({state: 'visible'}); + }); + + test('should disappear when clicking on the breadcrumb button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + await breadcrumbButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + test('should contain the selected value and the facet name in the breadcrumb button', async ({ breadbox, }) => { @@ -223,6 +339,20 @@ test.describe('Default', () => { await expect(breadcrumbButton).not.toBeVisible(); }); + test('should display the "Clear all" button', async ({breadbox}) => { + await expect(breadbox.getClearAllButton()).toBeVisible(); + }); + + test('should disappear when clicking on the "Clear all" button', async ({ + breadbox, + }) => { + const breadcrumbButton = breadbox.getBreadcrumbButtons(firstValueText); + const clearButton = breadbox.getClearAllButton(); + await clearButton.click(); + + await expect(breadcrumbButton).not.toBeVisible(); + }); + test('should contain the selected value and the facet name in the breadcrumb button', async ({ breadbox, }) => { diff --git a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts index fd26f53c28e..ff0866b9ef9 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-breadbox/e2e/page-object.ts @@ -6,6 +6,26 @@ export class AtomicCommerceBreadboxPageObject extends BasePageObject<'atomic-com super(page, 'atomic-commerce-breadbox'); } + async applyManualNumericalRange(min: number, max: number) { + const facetLocator = this.page.locator('atomic-commerce-numeric-facet'); + + const minInputLocator = facetLocator.getByLabel( + 'Enter a minimum numerical value for the Price facet' + ); + + const maxInputLocator = facetLocator.getByLabel( + 'Enter a maximum numerical value for the Price facet' + ); + + const applyButtonLocator = facetLocator.getByLabel( + 'Apply custom numerical values for the Price facet' + ); + + await minInputLocator.fill(String(min)); + await maxInputLocator.fill(String(max)); + await applyButtonLocator.click(); + } + getFacetValue( facetType: | 'regular' diff --git a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.new.stories.tsx b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.new.stories.tsx index 44488284583..06571fc064d 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.new.stories.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.new.stories.tsx @@ -4,28 +4,50 @@ import { } from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; -import {CommerceEngineConfiguration} from '@coveo/headless/dist/definitions/commerce.index'; import type {Meta, StoryObj as Story} from '@storybook/web-components'; import {html} from 'lit-html/static.js'; +// TODO KIT-3640 - Add stories for table display + const {decorator, play} = wrapInCommerceInterface({skipFirstSearch: false}); -const noResultsEngineConfig: Partial = { - preprocessRequest: (r) => { - const parsed = JSON.parse(r.body as string); - // eslint-disable-next-line @cspell/spellchecker - parsed.query = 'xqyzpqwlnftguscaqmzpbyuagloxhf'; - r.body = JSON.stringify(parsed); - return r; - }, -}; +const {play: playNoFirstQuery} = wrapInCommerceInterface({ + skipFirstSearch: true, +}); -const {play: playNoresults} = wrapInCommerceInterface({ +const {play: playNoProducts} = wrapInCommerceInterface({ skipFirstSearch: false, - engineConfig: noResultsEngineConfig, + engineConfig: { + preprocessRequest: (r) => { + const parsed = JSON.parse(r.body as string); + parsed.query = 'show me no products'; + r.body = JSON.stringify(parsed); + return r; + }, + }, }); const meta: Meta = { + argTypes: { + 'attributes-display': { + options: ['grid', 'list', 'table'], + control: {type: 'radio'}, + }, + 'attributes-density': { + options: ['compact', 'comfortable', 'normal'], + control: {type: 'radio'}, + }, + 'attributes-image-size': { + options: ['small', 'large', 'icon', 'none'], + control: {type: 'radio'}, + }, + }, + args: { + 'attributes-number-of-placeholders': 24, + 'attributes-display': 'grid', + 'attributes-density': 'normal', + 'attributes-image-size': 'small', + }, component: 'atomic-commerce-product-list', title: 'Atomic-Commerce/ProductList', id: 'atomic-commerce-product-list', @@ -38,55 +60,126 @@ const meta: Meta = { export default meta; export const Default: Story = { - name: 'atomic-commerce-product-list', + name: 'Grid display', play: async (context) => { await play(context); await playExecuteFirstSearch(context); }, }; -const {play: playNoFirstSearch} = wrapInCommerceInterface({ - skipFirstSearch: true, - engineConfig: noResultsEngineConfig, -}); +export const GridDisplayWithTemplate: Story = { + name: 'Grid display with template', + args: { + 'slots-default': ` + +`, + }, +}; -export const NoFirstSearch: Story = { - name: 'atomic-commerce-product-list', +export const GridDisplayBeforeQuery: Story = { + name: 'Grid display before query', play: async (context) => { - await playNoFirstSearch(context); + await playNoFirstQuery(context); }, }; -export const OpenInNewTab: Story = { - name: 'Open Product in New Tab', - tags: ['test'], - args: {'attributes-grid-cell-link-target': '_blank'}, +export const ListDisplay: Story = { + name: 'List display', play: async (context) => { - await wrapInCommerceInterface({skipFirstSearch: true}).play(context); + await play(context); await playExecuteFirstSearch(context); }, + args: { + 'attributes-display': 'list', + }, +}; + +export const ListDisplayWithTemplate: Story = { + name: 'List display with template', + args: { + 'attributes-display': 'list', + 'slots-default': ` + +`, + }, }; -export const NoResults: Story = { - name: 'No Results', - tags: ['test'], +export const ListDisplayBeforeQuery: Story = { + name: 'List display before query', + args: { + 'attributes-display': 'list', + }, + play: async (context) => { + await playNoFirstQuery(context); + }, +}; + +export const NoProducts: Story = { + name: 'No products', decorators: [(story) => story()], play: async (context) => { - await playNoresults(context); + await playNoProducts(context); await playExecuteFirstSearch(context); }, }; export const InPage: Story = { - name: 'In a page', - tags: ['test'], + name: 'In page', decorators: [ (story) => html` + + + + + + + + ${story()} diff --git a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx index dfbf9c8f23f..096a4398ef3 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-product-list/atomic-commerce-product-list.tsx @@ -52,6 +52,10 @@ import {SelectChildProductEventArgs} from '../product-template-components/atomic /** * @alpha * The `atomic-commerce-product-list` component is responsible for displaying products. + * + * @part result-list - The element containing the list of products. + * + * @slot default - The default slot where the product templates are defined. */ @Component({ tag: 'atomic-commerce-product-list', @@ -94,7 +98,7 @@ export class AtomicCommerceProductList /** * The desired layout to use when displaying products. Layouts affect how many products to display per row and how visually distinct they are from each other. */ - @Prop({reflect: true}) display: ItemDisplayLayout = 'grid'; + @Prop({reflect: true}) display: ItemDisplayLayout = 'grid'; // TODO KIT-3640 - Support 'table', or use ItemDisplayBasicLayout type. /** * The spacing of various elements in the product list, including the gap between products, the gap between parts of a product, and the font sizes of different parts in a product. diff --git a/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/atomic-commerce-product-list.e2e.ts b/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/atomic-commerce-product-list.e2e.ts index 601466f594c..48880549595 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/atomic-commerce-product-list.e2e.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/atomic-commerce-product-list.e2e.ts @@ -1,44 +1,206 @@ import {test, expect} from './fixture'; -test.describe('when no first search has yet been executed', async () => { - test.beforeEach(async ({productList}) => { - await productList.load({story: 'no-first-search'}); - await productList.hydrated.waitFor(); - }); +// TODO KIT-3640 - Add tests for table display - test('should display placeholders', async ({productList}) => { - await expect(productList.placeholders.first()).toBeVisible(); - }); +test.describe('before the query', async () => { + test.describe('when display is set to "grid"', () => { + test.beforeEach(async ({productList}) => { + await productList.load({story: 'grid-display-before-query'}); + await productList.hydrated.waitFor(); + }); - test('should display the default amount of placeholders', async ({ - productList, - }) => { - await expect(productList.placeholders.nth(23)).toBeVisible(); - }); -}); + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should show the default number of placeholders', async ({ + productList, + }) => { + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(24); + for (const placeholder of await productList.placeholders.all()) { + await expect(placeholder).toBeVisible(); + } + }); + + test('when numberOfPlaceholders is specified, should show the specified number of placeholders', async ({ + productList, + }) => { + await productList.load({ + story: 'grid-display-before-query', + args: {numberOfPlaceholders: 1}, + }); + await productList.hydrated.waitFor(); + + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(1); -test.describe('when executing an initial search', () => { - test.beforeEach(async ({productList, searchBox}) => { - await productList.load({story: 'in-page'}); - await searchBox.searchInput.fill('pants'); - await searchBox.submitButton.click(); - await productList.hydrated.waitFor(); + await expect(productList.placeholders.first()).toBeVisible(); + }); }); - test('should not display placeholders', async ({productList}) => { - await expect(productList.placeholders.first()).not.toBeVisible(); + test.describe('when display is set to "list"', () => { + test.beforeEach(async ({productList}) => { + await productList.load({ + story: 'list-display-before-query', + }); + await productList.hydrated.waitFor(); + }); + + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should show the default number of placeholders', async ({ + productList, + }) => { + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(24); + for (const placeholder of await productList.placeholders.all()) { + await expect(placeholder).toBeVisible(); + } + }); + + test('when numberOfPlaceholders is specified, should show the specified number of placeholders', async ({ + productList, + }) => { + await productList.load({ + story: 'list-display-before-query', + args: {numberOfPlaceholders: 1}, + }); + await productList.hydrated.waitFor(); + + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(1); + + await expect(productList.placeholders.first()).toBeVisible(); + }); }); }); -test.describe('when interface load yields no products', () => { - test.beforeEach(async ({productList}) => { - await productList.load({story: 'no-results'}); +test.describe('after the query', async () => { + test.describe('when there are products', () => { + test.describe('when display is set to "grid"', () => { + test.describe('when a template is not provided', () => { + test.beforeEach(async ({productList}) => { + await productList.load({story: 'default'}); + await productList.hydrated.waitFor(); + }); + + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should not have any placeholders', async ({productList}) => { + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(0); + }); + + test('should show all products', async ({productList}) => { + await expect + .poll(async () => await productList.products.count()) + .toBeGreaterThan(0); + for (const product of await productList.products.all()) { + await expect(product).toBeVisible(); + } + }); + }); + test.describe('when a template is provided', () => { + test.beforeEach(async ({productList}) => { + await productList.load({story: 'grid-display-with-template'}); + await productList.hydrated.waitFor(); + }); + + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should show all products', async ({productList}) => { + await expect + .poll(async () => await productList.products.count()) + .toBeGreaterThan(0); + for (const product of await productList.products.all()) { + await expect(product).toBeVisible(); + } + }); + }); + }); + + test.describe('when display is set to "list"', () => { + test.describe('when a template is not provided', () => { + test.beforeEach(async ({productList}) => { + await productList.load({story: 'list-display'}); + await productList.hydrated.waitFor(); + }); + + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should not have any placeholders', async ({productList}) => { + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(0); + }); + + test('should show all products', async ({productList}) => { + await expect + .poll(async () => await productList.products.count()) + .toBeGreaterThan(0); + for (const product of await productList.products.all()) { + await expect(product).toBeVisible(); + } + }); + }); + + test.describe('when a template is provided', () => { + test.beforeEach(async ({productList}) => { + await productList.load({ + story: 'list-display-with-template', + }); + await productList.hydrated.waitFor(); + }); + + test('should be a11y compliant', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test('should show all products', async ({productList}) => { + await expect + .poll(async () => await productList.products.count()) + .toBeGreaterThan(0); + for (const product of await productList.products.all()) { + await expect(product).toBeVisible(); + } + }); + }); + }); }); - test('should not display placeholders', async ({productList}) => { - await expect(productList.placeholders.first()).not.toBeVisible(); - await expect(productList.placeholders.nth(23)).not.toBeVisible(); + test.describe('when there are no products', () => { + test.beforeEach(async ({productList}) => { + await productList.load({story: 'no-products'}); + }); + + test('should not have any placeholders', async ({productList}) => { + await expect + .poll(async () => await productList.placeholders.count()) + .toBe(0); + }); + + test('should not have any products', async ({productList}) => { + await expect.poll(async () => await productList.products.count()).toBe(0); + }); }); }); - -// TODO: KIT-3247 add the rest of E2E tests diff --git a/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/page-object.ts b/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/page-object.ts index 9afb658c749..a89f20a22f1 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/page-object.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-product-list/e2e/page-object.ts @@ -13,4 +13,18 @@ export class ProductListObject extends BasePageObject<'atomic-commerce-product-l get products() { return this.page.locator('atomic-product'); } + + async withNoProducts() { + await this.page.route('**/commerce/v2/listing', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products = []; + await route.fulfill({ + response, + json: body, + }); + }); + + return this; + } } diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx index 019393485b3..292951e93d1 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx @@ -322,10 +322,7 @@ export class AtomicCommerceRecommendationList this.imageSize ), content: this.productTemplateProvider.getTemplateContent(product), - linkContent: - this.display === 'grid' - ? this.productTemplateProvider.getLinkTemplateContent(product) - : this.productTemplateProvider.getEmptyLinkTemplateContent(), + linkContent: this.productTemplateProvider.getLinkTemplateContent(product), store: this.bindings.store, density: this.density, display: this.display, diff --git a/packages/atomic/src/components/commerce/atomic-product/atomic-product.pcss b/packages/atomic/src/components/commerce/atomic-product/atomic-product.pcss index 571ecf189e5..6c5f479588d 100644 --- a/packages/atomic/src/components/commerce/atomic-product/atomic-product.pcss +++ b/packages/atomic/src/components/commerce/atomic-product/atomic-product.pcss @@ -14,11 +14,21 @@ } } &.display-grid { + &.image-large, + &.image-small, + &.image-icon, + &.image-none { + atomic-product-section-name { + min-height: calc(var(--line-height) * 2); + -webkit-line-clamp: 2; + line-clamp: 2; + } + } @screen desktop-only { &.image-large { atomic-product-section-children .product-child { @mixin aspect-ratio-h 1 / 1, auto; - width: 33%; + width: 16.65%; } } @@ -39,11 +49,17 @@ } @screen mobile-only { - &.image-large, + &.image-large { + atomic-product-section-children .product-child { + @mixin aspect-ratio-h 1 / 1, auto; + width: 16.65%; + } + } &.image-small { atomic-product-section-children .product-child { @mixin aspect-ratio-h 1 / 1, auto; width: 16.65%; + max-width: 4.75rem; } } @@ -55,6 +71,38 @@ } } } + + &.density-comfortable { + &.image-icon, + &.image-none, + &.image-small, + &.image-large { + & atomic-product-section-description { + margin-top: 1.25rem; + } + } + } + &.density-normal { + &.image-icon, + &.image-none, + &.image-small, + &.image-large { + & atomic-product-section-description { + margin-top: 0.75rem; + } + } + } + + &.density-compact { + &.image-icon, + &.image-none, + &.image-small, + &.image-large { + & atomic-product-section-description { + margin-top: 0.25rem; + } + } + } } &.display-list { diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx b/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx index a00bb87e78a..f1c00ecfc7a 100644 --- a/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx +++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx @@ -133,6 +133,7 @@ export class AtomicCommerceFacets implements InitializableComponent { > ); default: { + // TODO COMHUB-291 support location facet this.bindings.engine.logger.warn('Unexpected facet type.'); return; } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.pcss index 7a0133e5e82..1d52edb1bec 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.pcss +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.pcss @@ -1 +1,26 @@ @import '../../../../global/global.pcss'; + +atomic-product-children .children-container { + display: flex; + flex-wrap: wrap; +} + +.display-grid { + & atomic-product-children .children-container .product-child  { + &:nth-child(n + 6) { + display: none; + } + &:nth-child(6) { + & ~ .plus-button { + display: block; + } + &:last-child ~ .plus-button { + display: none; + } + } + } +} + +atomic-product-children .children-container .plus-button { + display: none; +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx index 26331cc6102..aa62e5724fc 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx @@ -12,12 +12,14 @@ import { Event, EventEmitter, State, + Host, } from '@stencil/core'; import { InitializableComponent, InitializeBindings, } from '../../../../utils/initialization-utils'; import {filterProtocol} from '../../../../utils/xss-utils'; +import {Button} from '../../../common/button'; import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; import {ProductContext} from '../product-template-decorators'; @@ -164,10 +166,15 @@ export class AtomicProductChildren } return ( -
+ {this.label.trim() !== '' && this.renderLabel()} -
{this.children.map((child) => this.renderChild(child))}
-
+
+ {this.children.map((child) => this.renderChild(child))} + +
+ ); } } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.new.stories.tsx new file mode 100644 index 00000000000..1f4ca81debf --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.new.stories.tsx @@ -0,0 +1,68 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import {updateQuery} from '../../../../../../headless/src/features/commerce/query/query-actions'; + +const { + decorator: commerceInterfaceDecorator, + play: initializeCommerceInterface, +} = wrapInCommerceInterface({ + skipFirstSearch: true, +}); + +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: productTemplateDecorator} = wrapInProductTemplate(); + +const meta: Meta = { + component: 'atomic-product-description', + title: 'Atomic-Commerce/Product Template Components/ProductDescription', + id: 'atomic-product-description', + render: renderComponent, + parameters, + argTypes: { + 'attributes-truncate-after': { + name: 'truncate-after', + type: 'string', + }, + 'attributes-field': { + name: 'field', + type: 'string', + }, + 'attributes-is-collapsible': { + name: 'is-collapsible', + type: 'boolean', + }, + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-description', + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, + decorators: [ + productTemplateDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + play: async (context) => { + await initializeCommerceInterface(context); + + const searchInterface = context.canvasElement.querySelector( + 'atomic-commerce-interface' + ); + searchInterface?.engine?.dispatch(updateQuery({query: 'boat'})); + + await searchInterface!.executeFirstRequest(); + }, + args: { + 'attributes-field': 'ec_description', + }, +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.pcss index 7a0133e5e82..b3c7cb73cde 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.pcss +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.pcss @@ -1 +1 @@ -@import '../../../../global/global.pcss'; +@import '../../../common/expandable-text/expandable-text.pcss'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.tsx index 770a7fcb127..b8675e54bcc 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/atomic-product-description.tsx @@ -1,13 +1,15 @@ import {Schema, StringValue} from '@coveo/bueno'; import {Product} from '@coveo/headless/commerce'; import {Component, State, h, Element, Prop} from '@stencil/core'; -import PlusIcon from '../../../../images/plus.svg'; -import {getFieldValueCaption} from '../../../../utils/field-utils'; import { InitializableComponent, InitializeBindings, } from '../../../../utils/initialization-utils'; -import {Button} from '../../../common/button'; +import { + ExpandableText, + TruncateAfter, +} from '../../../common/expandable-text/expandable-text'; +import {Hidden} from '../../../common/hidden'; import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; import {ProductContext} from '../product-template-decorators'; @@ -39,13 +41,18 @@ export class AtomicProductDescription /** * The number of lines after which the product description should be truncated. A value of "none" will disable truncation. */ - @Prop() public truncateAfter: 'none' | '1' | '2' | '3' | '4' = '2'; + @Prop() public truncateAfter: TruncateAfter = '2'; /** * The name of the description field to use. */ @Prop() public field: 'ec_description' | 'ec_shortdesc' = 'ec_shortdesc'; + /** + * Whether the description should be collapsible after being expanded. + */ + @Prop() public isCollapsible = true; + constructor() { this.resizeObserver = new ResizeObserver(() => { if ( @@ -65,7 +72,9 @@ export class AtomicProductDescription truncateAfter: new StringValue({ constrainTo: ['none', '1', '2', '3', '4'], }), - field: new StringValue({constrainTo: ['ec_shortdesc', 'ec_description']}), + field: new StringValue({ + constrainTo: ['ec_shortdesc', 'ec_description'], + }), }).validate({ truncateAfter: this.truncateAfter, field: this.field, @@ -74,7 +83,7 @@ export class AtomicProductDescription componentDidLoad() { this.descriptionText = this.hostElement.querySelector( - '.product-description-text' + '.expandable-text' ) as HTMLDivElement; if (this.descriptionText) { this.resizeObserver.observe(this.descriptionText); @@ -89,62 +98,29 @@ export class AtomicProductDescription this.isExpanded = !this.isExpanded; } - private getLineClampClass() { - const lineClampMap: Record = { - none: 'line-clamp-none', - 1: 'line-clamp-1', - 2: 'line-clamp-2', - 3: 'line-clamp-3', - 4: 'line-clamp-4', - }; - return lineClampMap[this.truncateAfter] || 'line-clamp-2'; - } - disconnectedCallback() { this.resizeObserver.disconnect(); } - private renderProductDescription() { - const productDescription = this.product[this.field] ?? ''; - - if (productDescription !== null) { - return ( - - ); + public render() { + const productDescription = this.product[this.field] ?? null; + + if (!productDescription) { + return ; } - } - private renderShowMoreButton() { return ( - - ); - } - - public render() { - return ( -
- {this.renderProductDescription()} - {this.renderShowMoreButton()} -
+ + ); } } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/atomic-product-description.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/atomic-product-description.e2e.ts new file mode 100644 index 00000000000..068dcc922fc --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/atomic-product-description.e2e.ts @@ -0,0 +1,173 @@ +import {test, expect} from './fixture'; + +test.describe('atomic-product-description', async () => { + test.beforeEach(async ({page, productDescription}) => { + await page.setViewportSize({width: 375, height: 667}); + await productDescription.load(); + await productDescription.hydrated.first().waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test.describe('when providing an invalid field', async () => { + test('should return error', async ({page, productDescription}) => { + await productDescription.load({ + args: { + // @ts-expect-error needed to test error on invalid field + field: 'ec_name', + }, + }); + + const errorMessage = await page.waitForEvent('console', (msg) => { + return msg.type() === 'error'; + }); + + expect(errorMessage.text()).toContain( + 'field: value should be one of: ec_shortdesc, ec_description.' + ); + }); + }); + test.describe('when providing an invalid truncate-after value', async () => { + const invalidValues = ['foo', '0', '-1', '5']; + + invalidValues.forEach((value) => { + test(`should return error for value: ${value}`, async ({ + page, + productDescription, + }) => { + await productDescription.load({ + args: { + // @ts-expect-error needed to test error on invalid value + truncateAfter: value, + }, + }); + + const errorMessage = await page.waitForEvent('console', (msg) => { + return msg.type() === 'error'; + }); + + expect(errorMessage.text()).toContain( + 'truncateAfter: value should be one of: none, 1, 2, 3, 4' + ); + }); + }); + }); + + const fields: Array<'ec_description' | 'ec_shortdesc'> = [ + 'ec_description', + 'ec_shortdesc', + ]; + + fields.forEach((field) => { + test(`should render description for ${field} field`, async ({ + productDescription, + }) => { + await productDescription.load({args: {field}}); + await productDescription.hydrated.first().waitFor(); + + expect(productDescription.textContent.first()).toBeVisible(); + }); + }); + + test.describe('when description is truncated', async () => { + const truncateValues: Array<{ + value: '1' | '4'; + expectedClass: RegExp; + }> = [ + {value: '1', expectedClass: /line-clamp-1/}, + {value: '4', expectedClass: /line-clamp-4/}, + ]; + + truncateValues.forEach(({value, expectedClass}) => { + test.beforeEach(async ({productDescription}) => { + await productDescription.withLongDescription(); + }); + test.describe(`when truncateAfter is set to ${value}`, async () => { + test(`should truncate description after ${value} lines`, async ({ + productDescription, + }) => { + await productDescription.load({ + args: {truncateAfter: value}, + }); + await productDescription.hydrated.first().waitFor(); + + const descriptionText = productDescription.textContent.first(); + expect(descriptionText).toHaveClass(expectedClass); + }); + + test('should show "Show More" button', async ({productDescription}) => { + const showMoreButton = productDescription.showMoreButton.first(); + expect(showMoreButton).toBeVisible(); + }); + + test.describe('when clicking the "Show More" button', async () => { + test.describe('when isCollapsible is true', async () => { + test.beforeEach(async ({productDescription}) => { + await productDescription.load({ + args: {truncateAfter: value, isCollapsible: true}, + }); + await productDescription.hydrated.first().waitFor(); + await productDescription.showMoreButton.first().click(); + }); + + test('should expand description', async ({productDescription}) => { + const descriptionText = productDescription.textContent.first(); + expect(descriptionText).not.toHaveClass(expectedClass); + }); + + test('should show "Show Less" button', async ({ + productDescription, + }) => { + const showLessButton = productDescription.showLessButton.first(); + expect(showLessButton).toBeVisible(); + }); + + test('should collapse description when clicking the "Show Less" button', async ({ + productDescription, + }) => { + const descriptionText = productDescription.textContent.first(); + await productDescription.showLessButton.first().click(); + + expect(descriptionText).toHaveClass(expectedClass); + }); + }); + + test.describe('when isCollapsible is false', async () => { + test.beforeEach(async ({productDescription}) => { + await productDescription.load({ + args: {truncateAfter: value, isCollapsible: false}, + }); + await productDescription.hydrated.first().waitFor(); + await productDescription.showMoreButton.first().click(); + }); + + test('should expand description', async ({productDescription}) => { + const descriptionText = productDescription.textContent.first(); + expect(descriptionText).not.toHaveClass(expectedClass); + }); + + test('should not show "Show Less" button', async ({ + productDescription, + }) => { + expect(productDescription.showLessButton).not.toBeVisible(); + }); + }); + }); + }); + }); + }); + + test.describe('when description is not truncated', async () => { + test('should hide "Show More" button ', async ({productDescription}) => { + await productDescription.load({ + args: {truncateAfter: 'none'}, + }); + await productDescription.hydrated.first().waitFor(); + + expect(productDescription.showMoreButton).not.toBeVisible(); + }); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/fixture.ts new file mode 100644 index 00000000000..0641f0049af --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/fixture.ts @@ -0,0 +1,19 @@ +import {test as base} from '@playwright/test'; +import { + makeAxeBuilder, + AxeFixture, +} from '../../../../../../playwright-utils/base-fixture'; +import {ProductDescriptionPageObject as ProductDescription} from './page-object'; + +type MyFixtures = { + productDescription: ProductDescription; +}; + +export const test = base.extend({ + makeAxeBuilder, + productDescription: async ({page}, use) => { + await use(new ProductDescription(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/page-object.ts new file mode 100644 index 00000000000..1105d13a0ef --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-description/e2e/page-object.ts @@ -0,0 +1,39 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class ProductDescriptionPageObject extends BasePageObject<'atomic-product-description'> { + constructor(page: Page) { + super(page, 'atomic-product-description'); + } + + get textContent() { + return this.page.locator('.expandable-text'); + } + + get highlightedText() { + return this.page.locator('atomic-product-text b'); + } + + get showMoreButton() { + return this.page.getByRole('button', {name: 'Show more'}); + } + + get showLessButton() { + return this.page.getByRole('button', {name: 'Show less'}); + } + + async withLongDescription() { + await this.page.route('**/commerce/v2/search', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0].ec_description = + 'This is a long description that should be truncated'.repeat(10); + await route.fulfill({ + response, + json: body, + }); + }); + + return this; + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.new.stories.tsx new file mode 100644 index 00000000000..0b46b949bbf --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.new.stories.tsx @@ -0,0 +1,57 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import {updateQuery} from '../../../../../../headless/src/features/commerce/query/query-actions'; + +const { + decorator: commerceInterfaceDecorator, + play: initializeCommerceInterface, +} = wrapInCommerceInterface({ + skipFirstSearch: true, +}); + +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: productTemplateDecorator} = wrapInProductTemplate(); + +const meta: Meta = { + component: 'atomic-product-excerpt', + title: 'Atomic-Commerce/Product Template Components/ProductExcerpt', + id: 'atomic-product-excerpt', + render: renderComponent, + parameters, + argTypes: { + 'attributes-truncate-after': { + name: 'truncate-after', + type: 'string', + }, + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-excerpt', + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, + decorators: [ + productTemplateDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + play: async (context) => { + await initializeCommerceInterface(context); + + const searchInterface = context.canvasElement.querySelector( + 'atomic-commerce-interface' + ); + searchInterface?.engine?.dispatch(updateQuery({query: 'kayak'})); + + await searchInterface!.executeFirstRequest(); + }, +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.pcss new file mode 100644 index 00000000000..b3c7cb73cde --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.pcss @@ -0,0 +1 @@ +@import '../../../common/expandable-text/expandable-text.pcss'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.tsx new file mode 100644 index 00000000000..d7d4c285550 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/atomic-product-excerpt.tsx @@ -0,0 +1,117 @@ +import {Schema, StringValue} from '@coveo/bueno'; +import {Product} from '@coveo/headless/commerce'; +import {Component, State, h, Element, Prop} from '@stencil/core'; +import { + InitializableComponent, + InitializeBindings, +} from '../../../../utils/initialization-utils'; +import { + ExpandableText, + TruncateAfter, +} from '../../../common/expandable-text/expandable-text'; +import {Hidden} from '../../../common/hidden'; +import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; +import {ProductContext} from '../product-template-decorators'; + +/** + * @alpha + * The `atomic-product-excerpt` component renders the excerpt of a product generated at query time. + */ +@Component({ + tag: 'atomic-product-excerpt', + styleUrl: 'atomic-product-excerpt.pcss', + shadow: false, +}) +export class AtomicProductExcerpt + implements InitializableComponent +{ + @InitializeBindings() public bindings!: CommerceBindings; + @ProductContext() private product!: Product; + + @Element() hostElement!: HTMLElement; + + public error!: Error; + + @State() private isExpanded = false; + @State() private isTruncated = false; + + private excerptText!: HTMLDivElement; + private resizeObserver: ResizeObserver; + + /** + * The number of lines after which the product excerpt should be truncated. A value of "none" will disable truncation. + */ + @Prop() public truncateAfter: TruncateAfter = '2'; + + /** + * Whether the excerpt should be collapsible after being expanded. + */ + @Prop() public isCollapsible = false; + + constructor() { + this.resizeObserver = new ResizeObserver(() => { + if ( + this.excerptText && + this.excerptText.scrollHeight > this.excerptText.offsetHeight + ) { + this.isTruncated = true; + } else { + this.isTruncated = false; + } + }); + this.validateProps(); + } + + private validateProps() { + new Schema({ + truncateAfter: new StringValue({ + constrainTo: ['none', '1', '2', '3', '4'], + }), + }).validate({ + truncateAfter: this.truncateAfter, + }); + } + + componentDidLoad() { + this.excerptText = this.hostElement.querySelector( + '.expandable-text' + ) as HTMLDivElement; + if (this.excerptText) { + this.resizeObserver.observe(this.excerptText); + } + } + + private onToggleExpand(e?: MouseEvent) { + if (e) { + e.stopPropagation(); + } + + this.isExpanded = !this.isExpanded; + } + + disconnectedCallback() { + this.resizeObserver.disconnect(); + } + + public render() { + const productExcerpt = this.product['excerpt'] ?? null; + + if (!productExcerpt) { + return ; + } + + return ( + this.onToggleExpand(e)} + showMoreLabel={this.bindings.i18n.t('show-more')} + showLessLabel={this.bindings.i18n.t('show-less')} + isCollapsible={this.isCollapsible} + > + + + ); + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/atomic-product-excerpt.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/atomic-product-excerpt.e2e.ts new file mode 100644 index 00000000000..fb8265375dc --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/atomic-product-excerpt.e2e.ts @@ -0,0 +1,143 @@ +import {test, expect} from './fixture'; + +test.describe('atomic-product-excerpt', async () => { + test.beforeEach(async ({page, productExcerpt}) => { + await page.setViewportSize({width: 200, height: 667}); + await productExcerpt.load(); + await productExcerpt.hydrated.first().waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test.describe('when providing an invalid truncate-after value', async () => { + const invalidValues = ['foo', '0', '-1', '5']; + + invalidValues.forEach((value) => { + test(`should return error for value: ${value}`, async ({ + page, + productExcerpt, + }) => { + await productExcerpt.load({ + args: { + // @ts-expect-error needed to test error on invalid value + truncateAfter: value, + }, + }); + + const errorMessage = await page.waitForEvent('console', (msg) => { + return msg.type() === 'error'; + }); + + expect(errorMessage.text()).toContain( + 'truncateAfter: value should be one of: none, 1, 2, 3, 4' + ); + }); + }); + }); + + test('should render excerpt text', async ({productExcerpt}) => { + await productExcerpt.hydrated.first().waitFor(); + + expect(productExcerpt.textContent.first()).toBeVisible(); + }); + + test.describe('when excerpt is truncated', async () => { + const truncateValues: Array<{ + value: '2' | '3'; + expectedClass: RegExp; + }> = [ + {value: '2', expectedClass: /line-clamp-2/}, + {value: '3', expectedClass: /line-clamp-3/}, + ]; + + truncateValues.forEach(({value, expectedClass}) => { + test.beforeEach(async ({productExcerpt}) => { + await productExcerpt.withLongExcerpt(); + }); + test.describe(`when truncateAfter is set to ${value}`, async () => { + test(`should truncate excerpt after ${value} lines`, async ({ + productExcerpt, + }) => { + await productExcerpt.load({ + args: {truncateAfter: value}, + }); + await productExcerpt.hydrated.first().waitFor(); + + const excerptText = productExcerpt.textContent.first(); + expect(excerptText).toHaveClass(expectedClass); + }); + + test('should show "Show More" button', async ({productExcerpt}) => { + const showMoreButton = productExcerpt.showMoreButton.first(); + expect(showMoreButton).toBeVisible(); + }); + + test.describe('when clicking the "Show More" button', async () => { + test.describe('when isCollapsible is true', async () => { + test.beforeEach(async ({productExcerpt}) => { + await productExcerpt.load({ + args: {truncateAfter: value, isCollapsible: true}, + }); + await productExcerpt.hydrated.first().waitFor(); + await productExcerpt.showMoreButton.first().click(); + }); + + test('should expand excerpt', async ({productExcerpt}) => { + const excerptText = productExcerpt.textContent.first(); + expect(excerptText).not.toHaveClass(expectedClass); + }); + + test('should show "Show Less" button', async ({productExcerpt}) => { + const showLessButton = productExcerpt.showLessButton.first(); + expect(showLessButton).toBeVisible(); + }); + + test('should collapse excerpt when clicking the "Show Less" button', async ({ + productExcerpt, + }) => { + const excerptText = productExcerpt.textContent.first(); + await productExcerpt.showLessButton.first().click(); + + expect(excerptText).toHaveClass(expectedClass); + }); + }); + + test.describe('when isCollapsible is false', async () => { + test.beforeEach(async ({productExcerpt}) => { + await productExcerpt.load({ + args: {truncateAfter: value, isCollapsible: false}, + }); + await productExcerpt.hydrated.first().waitFor(); + await productExcerpt.showMoreButton.first().click(); + }); + + test('should expand excerpt', async ({productExcerpt}) => { + const excerptText = productExcerpt.textContent.first(); + expect(excerptText).not.toHaveClass(expectedClass); + }); + + test('should not show "Show Less" button', async ({ + productExcerpt, + }) => { + expect(productExcerpt.showLessButton).not.toBeVisible(); + }); + }); + }); + }); + }); + }); + + test.describe('when excerpt is not truncated', async () => { + test('should hide "Show More" button ', async ({productExcerpt}) => { + await productExcerpt.load({ + args: {truncateAfter: 'none'}, + }); + await productExcerpt.hydrated.first().waitFor(); + + expect(productExcerpt.showMoreButton).not.toBeVisible(); + }); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/fixture.ts new file mode 100644 index 00000000000..f04f298d0cd --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/fixture.ts @@ -0,0 +1,19 @@ +import {test as base} from '@playwright/test'; +import { + makeAxeBuilder, + AxeFixture, +} from '../../../../../../playwright-utils/base-fixture'; +import {ProductExcerptPageObject as ProductExcerpt} from './page-object'; + +type MyFixtures = { + productExcerpt: ProductExcerpt; +}; + +export const test = base.extend({ + makeAxeBuilder, + productExcerpt: async ({page}, use) => { + await use(new ProductExcerpt(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/page-object.ts new file mode 100644 index 00000000000..93e6ce315bf --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-excerpt/e2e/page-object.ts @@ -0,0 +1,39 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class ProductExcerptPageObject extends BasePageObject<'atomic-product-excerpt'> { + constructor(page: Page) { + super(page, 'atomic-product-excerpt'); + } + + get textContent() { + return this.page.locator('.expandable-text'); + } + + get highlightedText() { + return this.page.locator('atomic-product-text b'); + } + + get showMoreButton() { + return this.page.getByRole('button', {name: 'Show more'}); + } + + get showLessButton() { + return this.page.getByRole('button', {name: 'Show less'}); + } + + async withLongExcerpt() { + await this.page.route('**/commerce/v2/search', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0].excerpt = + 'This is a long excerpt that should be truncated'.repeat(10); + await route.fulfill({ + response, + json: body, + }); + }); + + return this; + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-field-condition/atomic-product-field-condition.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-field-condition/atomic-product-field-condition.tsx index 4644dcadbfc..7b2bf7e8a01 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-field-condition/atomic-product-field-condition.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-field-condition/atomic-product-field-condition.tsx @@ -28,9 +28,19 @@ export class AtomicProductFieldCondition { */ @Prop({reflect: true}) ifNotDefined?: string; - @MapProp({splitValues: true}) mustMatch: Record = {}; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) mustMatch: Record = + {}; - @MapProp({splitValues: true}) mustNotMatch: Record = {}; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) mustNotMatch: Record = + {}; private conditions: ProductTemplateCondition[] = []; private shouldBeRemoved = false; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx new file mode 100644 index 00000000000..10de7eb7be0 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.new.stories.tsx @@ -0,0 +1,106 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import {getSampleCommerceEngineConfiguration} from '@coveo/headless/commerce'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import {html} from 'lit/static-html.js'; + +const baseConfiguration = getSampleCommerceEngineConfiguration(); + +const {decorator: productDecorator} = wrapInProductTemplate(); +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: commerceInterfaceDecorator, play} = wrapInCommerceInterface({ + engineConfig: { + ...baseConfiguration, + context: { + ...baseConfiguration.context, + view: { + url: 'https://sports.barca.group/browse/promotions/ui-kit-testing-product-multi-value-text', + }, + }, + }, + type: 'product-listing', +}); + +const meta: Meta = { + component: 'atomic-product-multi-value-text', + title: 'Atomic-Commerce/Product Template Components/MultiValueText', + id: 'atomic-product-multi-value-text', + render: renderComponent, + parameters, + play, + args: { + 'attributes-field': 'cat_available_sizes', + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-multi-value-text', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], +}; + +export const WithDelimiter: Story = { + name: 'With delimiter', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-field': 'ec_product_id', + 'attributes-delimiter': '_', + }, +}; + +export const WithMaxValuesToDisplaySetToMinimum: Story = { + name: 'With max values to display set to minimum', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-max-values-to-display': 1, + }, +}; + +export const WithMaxValuesToDisplaySetToTotalNumberOfValues: Story = { + name: 'With max values to display set to total number of values', + decorators: [ + productDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + args: { + 'attributes-max-values-to-display': 6, + }, +}; + +export const InAPageWithTheCorrespondingFacet: Story = { + name: 'In a page with the corresponding facet', + decorators: [ + productDecorator, + commerceProductListDecorator, + (story) => { + return html` + + + + + ${story()} + + + `; + }, + commerceInterfaceDecorator, + ], +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss new file mode 100644 index 00000000000..d8afb1185ec --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.pcss @@ -0,0 +1,19 @@ +:host { + > ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; + + li { + display: inline-block; + } + } +} + +.separator { + &::before { + display: inline; + content: ',\00a0'; + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx new file mode 100644 index 00000000000..8a6682d0869 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/atomic-product-multi-value-text.tsx @@ -0,0 +1,200 @@ +import { + BreadcrumbManager, + buildProductListing, + buildSearch, + Product, + ProductListing, + ProductTemplatesHelpers, + Search, + RegularFacetValue, +} from '@coveo/headless/commerce'; +import {Component, Element, Prop, h, State, VNode} from '@stencil/core'; +import {getFieldValueCaption} from '../../../../utils/field-utils'; +import {InitializeBindings} from '../../../../utils/initialization-utils'; +import {titleToKebab} from '../../../../utils/utils'; +import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; +import {ProductContext} from '../product-template-decorators'; + +/** + * The `atomic-product-multi-value-text` component renders the values of a multi-value string field. + * @part product-multi-value-text-list - The list of field values. + * @part product-multi-value-text-separator - The separator to display between each of the field values. + * @part product-multi-value-text-value - A field value. + * @part product-multi-value-text-value-more - A label indicating some values were omitted. + * @slot product-multi-value-text-value-* - A custom caption value that's specified for a given part of a multi-text field value. For example, if you want to use `Off-Campus Resident` as a caption value for `Off-campus apartment` in `Off-campus apartment;On-campus apartment`, you'd use `Off-Campus Resident`). The suffix of this slot corresponds with the field value, written in kebab case. + */ +@Component({ + tag: 'atomic-product-multi-value-text', + styleUrl: 'atomic-product-multi-value-text.pcss', + shadow: true, +}) +export class AtomicProductMultiValueText { + public breadcrumbManager!: BreadcrumbManager; + public searchOrListing!: Search | ProductListing; + + @InitializeBindings() public bindings!: CommerceBindings; + @ProductContext() private product!: Product; + + @Element() host!: HTMLElement; + + @State() public error!: Error; + + /** + * The field that the component should use. + * The component will try to find this field in the `Product.additionalFields` object unless it finds it in the `Product` object first. + * Make sure this field is present in the `fieldsToInclude` property of the `atomic-commerce-interface` component. + */ + @Prop({reflect: true}) public field!: string; + + /** + * The maximum number of field values to display. + * If there are _n_ more values than the specified maximum, the last displayed value will be "_n_ more...". + */ + @Prop({reflect: true}) public maxValuesToDisplay = 3; + + /** + * The delimiter used to separate values when the field isn't indexed as a multi value field. + */ + @Prop({reflect: true}) public delimiter: string | null = null; + + private sortedValues: string[] | null = null; + + public initialize() { + if (this.bindings.interfaceElement.type === 'product-listing') { + this.searchOrListing = buildProductListing(this.bindings.engine); + } else { + this.searchOrListing = buildSearch(this.bindings.engine); + } + + this.breadcrumbManager = this.searchOrListing.breadcrumbManager(); + } + + private get productValues() { + const value = ProductTemplatesHelpers.getProductProperty( + this.product, + this.field + ); + + if (value === null) { + return null; + } + + if (Array.isArray(value)) { + return value.map((v) => `${v}`.trim()); + } + + if (typeof value !== 'string' || value.trim() === '') { + this.error = new Error( + `Could not parse "${value}" from field "${this.field}" as a string array.` + ); + return null; + } + + return this.delimiter + ? value.split(this.delimiter).map((value) => value.trim()) + : [value]; + } + + private get facetSelectedValues() { + return this.breadcrumbManager.state.facetBreadcrumbs + .filter((facet) => facet.field === this.field) + .reduce( + (values, facet) => [ + ...values, + ...facet.values.map(({value}) => (value as RegularFacetValue).value), + ], + [] as string[] + ); + } + + private updateSortedValues() { + const allValues = this.productValues; + if (allValues === null) { + this.sortedValues = null; + return; + } + const firstValues = this.facetSelectedValues.filter((value) => + allValues.includes(value) + ); + this.sortedValues = Array.from(new Set([...firstValues, ...allValues])); + } + + private getShouldDisplayLabel(values: string[]) { + return ( + this.maxValuesToDisplay > 0 && values.length > this.maxValuesToDisplay + ); + } + + private getNumberOfValuesToDisplay(values: string[]) { + return Math.min(values.length, this.maxValuesToDisplay); + } + + private renderValue(value: string) { + const label = getFieldValueCaption(this.field, value, this.bindings.i18n); + const kebabValue = titleToKebab(value); + return ( +
  • + + {label} + +
  • + ); + } + + private renderSeparator(beforeValue: string, afterValue: string) { + return ( + + ); + } + + private renderMoreLabel(value: number) { + return ( +
  • + {this.bindings.i18n.t('n-more', {value})} +
  • + ); + } + + private renderListItems(values: string[]) { + const numberOfValuesToDisplay = this.getNumberOfValuesToDisplay(values); + + const nodes: VNode[] = []; + for (let i = 0; i < numberOfValuesToDisplay; i++) { + if (i > 0) { + nodes.push(this.renderSeparator(values[i - 1], values[i])); + } + nodes.push(this.renderValue(values[i])); + } + if (this.getShouldDisplayLabel(values)) { + nodes.push( + this.renderSeparator( + values[numberOfValuesToDisplay - 1], + 'more-field-values' + ) + ); + nodes.push(this.renderMoreLabel(values.length - numberOfValuesToDisplay)); + } + return nodes; + } + + public componentWillRender() { + this.updateSortedValues(); + } + + public render() { + if (this.sortedValues === null) { + this.host.remove(); + return; + } + return ( +
      + {...this.renderListItems(this.sortedValues)} +
    + ); + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts new file mode 100644 index 00000000000..3ca01b158c1 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/atomic-product-multi-value-text.e2e.ts @@ -0,0 +1,162 @@ +import {test, expect} from './fixture'; + +test.describe('default', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load(); + }); + + test('should render 3 values and 3 separators', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(3); + await expect(productMultiValueText.separators).toHaveCount(3); + }); + + test('should render an indicator that 3 more values are available', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.moreValuesIndicator(3)).toBeVisible(); + }); +}); + +test.describe('with a delimiter', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-delimiter', + }); + }); + test('when field value does not include the specified delimiter, should render as a single value', async ({ + productMultiValueText, + }) => { + await productMultiValueText.withCustomDelimiter({ + delimiter: '/', + field: 'ec_product_id', + values: ['a', 'b', 'c', 'd', 'e'], + }); + + await expect(productMultiValueText.values).toHaveCount(1); + await expect(productMultiValueText.separators).toHaveCount(0); + await expect(productMultiValueText.values.first()).toHaveText('a/b/c/d/e'); + await expect(productMultiValueText.moreValuesIndicator()).not.toBeVisible(); + }); + + test('when field value includes the specified delimiter, should render as distinct values', async ({ + productMultiValueText, + }) => { + await productMultiValueText.withCustomDelimiter({ + delimiter: '_', + field: 'ec_product_id', + values: ['a', 'b', 'c', 'd', 'e'], + }); + + await expect(productMultiValueText.values).toHaveCount(3); + await expect(productMultiValueText.separators).toHaveCount(3); + await expect(productMultiValueText.values.first()).toHaveText('a'); + await expect(productMultiValueText.values.nth(1)).toHaveText('b'); + await expect(productMultiValueText.values.nth(2)).toHaveText('c'); + await expect(productMultiValueText.moreValuesIndicator(2)).toBeVisible(); + }); +}); + +test.describe('with max-values-to-display set to minimum (1)', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-max-values-to-display-set-to-minimum', + }); + }); + + test('should render 1 value and 1 separator', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(1); + await expect(productMultiValueText.separators).toHaveCount(1); + }); + + test('should render an indicator that 5 more values are available', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.moreValuesIndicator(5)).toBeVisible(); + }); +}); + +test.describe('with max-values-to-display set to total number of values (6)', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'with-max-values-to-display-set-to-total-number-of-values', + }); + }); + + test('should be a11y compliant', async ({ + productMultiValueText, + makeAxeBuilder, + }) => { + await productMultiValueText.hydrated.waitFor(); + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations).toEqual([]); + }); + + test('should render 6 values and 5 separators', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values).toHaveCount(6); + await expect(productMultiValueText.separators).toHaveCount(5); + }); + + test('should not render an indicator that more values are available', async ({ + productMultiValueText, + }) => { + expect(productMultiValueText.moreValuesIndicator()).not.toBeVisible(); + }); +}); + +test.describe('in a page with corresponding facet', () => { + test.beforeEach(async ({productMultiValueText}) => { + await productMultiValueText.load({ + story: 'in-a-page-with-the-corresponding-facet', + }); + }); + + test('should be a11y compliant', async ({ + productMultiValueText, + makeAxeBuilder, + }) => { + await productMultiValueText.hydrated.waitFor(); + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations).toEqual([]); + }); + + test('with no selected values in corresponding facet, should render values in default order', async ({ + productMultiValueText, + }) => { + await expect(productMultiValueText.values.first()).toHaveText('XS'); + await expect(productMultiValueText.values.nth(1)).toHaveText('S'); + await expect(productMultiValueText.values.nth(2)).toHaveText('M'); + }); + + test('with a selected value in corresponding facet, should render that value first', async ({ + page, + productMultiValueText, + }) => { + await expect(productMultiValueText.values.first()).toHaveText('XS'); + + await page.getByLabel('Inclusion filter on L').click(); + + await expect(productMultiValueText.values.first()).toHaveText('L'); + }); + + test('with 3 selected values in corresponding facet, should render those values in alphabetical order', async ({ + productMultiValueText, + page, + }) => { + await page.getByLabel('Inclusion filter on M').click(); + await expect(page.getByText('Clear filter')).toBeVisible(); + await page.getByLabel('Inclusion filter on L').click(); + await expect(page.getByText('Clear 2 filters')).toBeVisible(); + await page.getByLabel('Inclusion filter on XL').click(); + await expect(page.getByText('Clear 3 filters')).toBeVisible(); + + await expect(productMultiValueText.values.first()).toHaveText('L'); + await expect(productMultiValueText.values.nth(1)).toHaveText('M'); + await expect(productMultiValueText.values.nth(2)).toHaveText('XL'); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts new file mode 100644 index 00000000000..7a714d4e8a2 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/fixture.ts @@ -0,0 +1,19 @@ +import {test as base} from '@playwright/test'; +import { + AxeFixture, + makeAxeBuilder, +} from '../../../../../../playwright-utils/base-fixture'; +import {ProductMultiValueTextPageObject} from './page-object'; + +type MyFixtures = { + productMultiValueText: ProductMultiValueTextPageObject; +}; + +export const test = base.extend({ + makeAxeBuilder, + productMultiValueText: async ({page}, use) => { + await use(new ProductMultiValueTextPageObject(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts new file mode 100644 index 00000000000..0762081d1a8 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-multi-value-text/e2e/page-object.ts @@ -0,0 +1,48 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class ProductMultiValueTextPageObject extends BasePageObject<'atomic-product-multi-value-text'> { + constructor(page: Page) { + super(page, 'atomic-product-multi-value-text'); + } + + get values() { + return this.hydrated + .first() + .locator('li[part="product-multi-value-text-value"]'); + } + + get separators() { + return this.hydrated.first().locator('li[class="separator"]'); + } + + moreValuesIndicator(expectedNumber?: number) { + return this.hydrated + .first() + .getByText( + `${expectedNumber ? expectedNumber.toString() + ' ' : ''}more...` + ); + } + + async withCustomDelimiter({ + delimiter, + values, + field, + }: { + delimiter: string; + values: string[]; + field: string; + }) { + await this.page.route('**/commerce/v2/listing', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.products[0][field] = values.join(delimiter); + await route.fulfill({ + response, + json: body, + }); + }); + + return this; + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.pcss b/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.pcss new file mode 100644 index 00000000000..40c60391368 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.pcss @@ -0,0 +1,11 @@ +.display-grid { + & atomic-product-price { + display: flex; + flex-wrap: wrap; + flex-direction: column; + + & .original-price { + line-height: 1; + } + } +} diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.tsx index 43fd9d1655b..8e782bf9033 100644 --- a/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.tsx +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-price/atomic-product-price.tsx @@ -4,7 +4,7 @@ import { Context, ContextState, } from '@coveo/headless/commerce'; -import {Component, h} from '@stencil/core'; +import {Component, h, Host} from '@stencil/core'; import { BindStateToController, InitializableComponent, @@ -21,6 +21,7 @@ import {parseValue} from '../product-utils'; */ @Component({ tag: 'atomic-product-price', + styleUrl: 'atomic-product-price.pcss', shadow: false, }) export class AtomicProductPrice @@ -82,18 +83,22 @@ export class AtomicProductPrice : null; return ( -
    +
    {mainPrice}
    - {originalPrice && ( -
    - {originalPrice} -
    - )} -
    + +
    + {originalPrice ?? '​'} +
    + ); } } diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/atomic-product-text.new.stories.tsx b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/atomic-product-text.new.stories.tsx new file mode 100644 index 00000000000..f0b94f8530e --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/atomic-product-text.new.stories.tsx @@ -0,0 +1,63 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {wrapInCommerceProductList} from '@coveo/atomic-storybook-utils/commerce/commerce-product-list-wrapper'; +import {wrapInProductTemplate} from '@coveo/atomic-storybook-utils/commerce/commerce-product-template-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import type {Meta, StoryObj as Story} from '@storybook/web-components'; +import {updateQuery} from '../../../../../../headless/src/features/commerce/query/query-actions'; + +const { + decorator: commerceInterfaceDecorator, + play: initializeCommerceInterface, +} = wrapInCommerceInterface({ + skipFirstSearch: true, +}); + +const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(); +const {decorator: productTemplateDecorator} = wrapInProductTemplate(); + +const meta: Meta = { + component: 'atomic-product-text', + title: 'Atomic-Commerce/Product Template Components/ProductText', + id: 'atomic-product-text', + render: renderComponent, + parameters, + argTypes: { + 'attributes-default': { + name: 'default', + type: 'string', + }, + 'attributes-field': { + name: 'field', + type: 'string', + }, + 'attributes-should-highlight': { + name: 'should-highlight', + type: 'boolean', + }, + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-product-text', + decorators: [ + productTemplateDecorator, + commerceProductListDecorator, + commerceInterfaceDecorator, + ], + play: async (context) => { + await initializeCommerceInterface(context); + + const searchInterface = context.canvasElement.querySelector( + 'atomic-commerce-interface' + ); + searchInterface?.engine?.dispatch(updateQuery({query: 'kayak'})); + + await searchInterface!.executeFirstRequest(); + }, + args: { + 'attributes-field': 'excerpt', + }, +}; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/atomic-product-text.e2e.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/atomic-product-text.e2e.ts new file mode 100644 index 00000000000..4e9f9406913 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/atomic-product-text.e2e.ts @@ -0,0 +1,95 @@ +import {test, expect} from './fixture'; + +test.describe('default', async () => { + test.beforeEach(async ({productText}) => { + await productText.load(); + await productText.hydrated.first().waitFor(); + }); + + test('should be accessible', async ({makeAxeBuilder}) => { + const accessibilityResults = await makeAxeBuilder().analyze(); + expect(accessibilityResults.violations.length).toEqual(0); + }); + + test.describe('when field has no value and default is set', async () => { + test('should render default text', async ({productText}) => { + await productText.load({ + args: {field: 'nonexistentField', default: 'Default Text'}, + }); + await productText.hydrated.first().waitFor(); + + expect(productText.textContent.first()).toContainText('Default Text'); + }); + }); +}); + +test.describe('when using a field that supports highlights', async () => { + const fields = ['excerpt', 'ec_name']; + + fields.forEach((field) => { + test.describe(`when displaying the ${field}`, async () => { + test.beforeEach(async ({productText}) => { + await productText.load({args: {field}}); + await productText.hydrated.first().waitFor(); + }); + + test(`should highlight the keywords in the ${field}`, async ({ + productText, + }) => { + const keywordPattern = /^kayak/i; + + const highlightedText = + await productText.highlightedText.allTextContents(); + + highlightedText.forEach((text) => { + expect(text).toMatch(keywordPattern); + }); + }); + }); + + test(`should not highlight the keywords in the ${field} when shouldHighlight is false`, async ({ + productText, + }) => { + await productText.load({ + args: {field: 'excerpt', shouldHighlight: false}, + }); + await productText.hydrated.first().waitFor(); + + expect(productText.textContent.first()).toContainText(/kayak/i); + + const highlightedText = + await productText.highlightedText.allTextContents(); + expect(highlightedText.length).toEqual(0); + }); + }); +}); + +test.describe('when displaying a field that does not support highlights', async () => { + test.beforeEach(async ({productText}) => { + await productText.load({args: {field: 'ec_description'}}); + await productText.hydrated.first().waitFor(); + }); + + test('should render the field value', async ({productText}) => { + expect(productText.textContent.first()).toBeVisible(); + }); + + test('should not highlight the keywords in the excerpt', async ({ + productText, + }) => { + const highlightedText = await productText.highlightedText.allTextContents(); + expect(productText.textContent.first()).toContainText(/kayak/i); + expect(highlightedText).not.toContain(/kayak/i); + }); +}); + +test.describe('when using a non-string field', async () => { + test.beforeEach(async ({productText, product}) => { + await productText.load({args: {field: 'ec_price'}}); + await product.hydrated.waitFor(); + }); + + test('should not render the field value', async ({productText}) => { + expect(productText.textContent.first()).not.toBeVisible(); + }); +}); diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/fixture.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/fixture.ts new file mode 100644 index 00000000000..ff281071361 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/fixture.ts @@ -0,0 +1,24 @@ +import {test as base} from '@playwright/test'; +import { + makeAxeBuilder, + AxeFixture, +} from '../../../../../../playwright-utils/base-fixture'; +import {ProductsPageObject as Product} from '../../../atomic-product/e2e/page-object'; +import {ProductTextPageObject as ProductText} from './page-object'; + +type MyFixtures = { + productText: ProductText; + product: Product; +}; + +export const test = base.extend({ + makeAxeBuilder, + productText: async ({page}, use) => { + await use(new ProductText(page)); + }, + product: async ({page}, use) => { + await use(new Product(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/page-object.ts b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/page-object.ts new file mode 100644 index 00000000000..9db396c5e24 --- /dev/null +++ b/packages/atomic/src/components/commerce/product-template-components/atomic-product-text/e2e/page-object.ts @@ -0,0 +1,16 @@ +import {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class ProductTextPageObject extends BasePageObject<'atomic-product-text'> { + constructor(page: Page) { + super(page, 'atomic-product-text'); + } + + get textContent() { + return this.page.locator('atomic-product-text'); + } + + get highlightedText() { + return this.page.locator('atomic-product-text b'); + } +} diff --git a/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/atomic-commerce-search-box-instant-products.new.stories.tsx b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/atomic-commerce-search-box-instant-products.new.stories.tsx new file mode 100644 index 00000000000..3cce3c40e2f --- /dev/null +++ b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/atomic-commerce-search-box-instant-products.new.stories.tsx @@ -0,0 +1,67 @@ +import {wrapInCommerceInterface} from '@coveo/atomic-storybook-utils/commerce/commerce-interface-wrapper'; +import {parameters} from '@coveo/atomic-storybook-utils/common/common-meta-parameters'; +import {renderComponent} from '@coveo/atomic-storybook-utils/common/render-component'; +import type { + Decorator, + Meta, + StoryObj as Story, +} from '@storybook/web-components'; +import {html} from 'lit/static-html.js'; + +const {decorator, play} = wrapInCommerceInterface({skipFirstSearch: true}); + +const wrapInSearchBox: Decorator = (story) => { + return html` + + + ${story()} + + `; +}; + +const meta: Meta = { + component: 'atomic-commerce-search-box-instant-products', + title: + 'Atomic-Commerce/Interface Components/atomic-commerce-search-box-instant-products', + id: 'atomic-commerce-search-box-instant-products', + render: renderComponent, + decorators: [wrapInSearchBox, decorator], + parameters, + play, + argTypes: { + 'attributes-density': { + name: 'density', + options: ['normal', 'comfortable', 'compact'], + }, + 'attributes-image-size': { + name: 'image-size', + options: ['icon', 'small', 'large', 'none'], + }, + 'attributes-aria-label-generator': { + name: 'aria-label-generator', + type: 'function', + }, + }, +}; + +export const WithComfortableDensity: Story = { + tags: ['test'], + name: 'With comfortable density', + args: { + 'attributes-density': 'comfortable', + }, +}; + +export const WithNoImage: Story = { + tags: ['test'], + name: 'With no image', + args: { + 'attributes-image-size': 'none', + }, +}; + +export default meta; + +export const Default: Story = { + name: 'atomic-commerce-search-box-instant-product', +}; diff --git a/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/atomic-commerce-search-box-instant-products.e2e.ts b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/atomic-commerce-search-box-instant-products.e2e.ts new file mode 100644 index 00000000000..432e7f4da70 --- /dev/null +++ b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/atomic-commerce-search-box-instant-products.e2e.ts @@ -0,0 +1,98 @@ +import {test, expect} from './fixture'; + +test.describe('default', () => { + test.beforeEach(async ({instantProduct, searchBox}) => { + await instantProduct.load(); + await searchBox.hydrated.waitFor(); + await searchBox.searchInput.click(); + }); + + test('should display instant products', async ({instantProduct}) => { + const products = await instantProduct.instantProducts.all(); + for (let i = 0; i < products.length; i++) { + await expect(products[i]).toBeVisible(); + } + }); + + test('should be clickable anywhere on the instant result component', async ({ + instantProduct, + page, + }) => { + await expect(instantProduct.instantProducts.first()).toBeEnabled(); + await instantProduct.instantProducts.first().click(); + await page.waitForURL(/https:\/\/sports\.barca\.group\/*/); + }); + + test.describe('with density set to comfortable', () => { + test.beforeEach(async ({instantProduct, searchBox}) => { + await instantProduct.load({story: 'with-comfortable-density'}); + await searchBox.hydrated.waitFor(); + await searchBox.searchInput.click(); + }); + + test('should apply comfortable density class', async ({instantProduct}) => { + await expect(instantProduct.productRoots.first()).toHaveClass( + /.*density-comfortable.*/ + ); + }); + }); + + test.describe('with imageSize set to none', () => { + test.beforeEach(async ({instantProduct, searchBox}) => { + await instantProduct.load({story: 'with-no-image'}); + await searchBox.hydrated.waitFor(); + await searchBox.searchInput.click(); + }); + + test('should apply no image class', async ({instantProduct}) => { + await expect(instantProduct.productRoots.first()).toHaveClass( + /.*image-none.*/ + ); + }); + }); + + test.describe('when clicking on "See All Results"', () => { + test.beforeEach(async ({instantProduct, searchBox}) => { + await instantProduct.load(); + await searchBox.component.evaluate((node) => + node.setAttribute( + 'redirection-url', + './iframe.html?id=atomic-commerce-search-box--in-page&viewMode=story' + ) + ); + await searchBox.hydrated.waitFor(); + await searchBox.searchInput.click(); + }); + test('should redirect to the specified url after selecting a suggestion', async ({ + instantProduct, + page, + }) => { + await instantProduct.showAllButton.click(); + await page.waitForURL( + '**/iframe.html?id=atomic-commerce-search-box--in-page*' + ); + }); + }); + + test.describe('with a custom aria label generator', async () => { + test.beforeEach(async ({instantProduct, searchBox}) => { + await instantProduct.load({ + args: {ariaLabelGenerator: () => 'custom-aria-label'}, + }); + await searchBox.hydrated.waitFor(); + await searchBox.searchInput.click(); + }); + + test('should update the instant product aria label', async ({ + instantProduct, + }) => { + const products = await instantProduct.instantProducts.all(); + for (let i = 0; i < products.length; i++) { + await expect(products[i]).toHaveAttribute( + 'aria-live', + 'custom-aria-label' + ); + } + }); + }); +}); diff --git a/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/fixture.ts b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/fixture.ts new file mode 100644 index 00000000000..2b33ec453ba --- /dev/null +++ b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/fixture.ts @@ -0,0 +1,24 @@ +import {test as base} from '@playwright/test'; +import { + AxeFixture, + makeAxeBuilder, +} from '../../../../../../playwright-utils/base-fixture'; +import {SearchBoxPageObject} from '../../../atomic-commerce-search-box/e2e/page-object'; +import {InstantProductPageObject} from './page-object'; + +type Fixture = { + searchBox: SearchBoxPageObject; + instantProduct: InstantProductPageObject; +}; + +export const test = base.extend({ + makeAxeBuilder, + instantProduct: async ({page}, use) => { + await use(new InstantProductPageObject(page)); + }, + searchBox: async ({page}, use) => { + await use(new SearchBoxPageObject(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/page-object.ts b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/page-object.ts new file mode 100644 index 00000000000..d5bf1e66b46 --- /dev/null +++ b/packages/atomic/src/components/commerce/search-box-suggestions/atomic-commerce-search-box-instant-products/e2e/page-object.ts @@ -0,0 +1,24 @@ +import type {Page} from '@playwright/test'; +import {BasePageObject} from '../../../../../../playwright-utils/base-page-object'; + +export class InstantProductPageObject extends BasePageObject<'atomic-commerce-search-box-instant-products'> { + constructor(page: Page) { + super(page, 'atomic-commerce-search-box-instant-products'); + } + + get component() { + return this.page.locator('atomic-commerce-search-box-instant-products'); + } + + get instantProducts() { + return this.page.getByLabel('instant result'); + } + + get productRoots() { + return this.page.locator('.result-root'); + } + + get showAllButton() { + return this.page.getByLabel('See all results'); + } +} diff --git a/packages/atomic/src/components/common/expandable-text/expandable-text.pcss b/packages/atomic/src/components/common/expandable-text/expandable-text.pcss new file mode 100644 index 00000000000..3e407f1b6b2 --- /dev/null +++ b/packages/atomic/src/components/common/expandable-text/expandable-text.pcss @@ -0,0 +1,19 @@ +.expandable-text { + line-height: var(--line-height); +} + +.min-lines-1 { + min-height: calc(var(--line-height) * 1); +} + +.min-lines-2 { + min-height: calc(var(--line-height) * 2); +} + +.min-lines-3 { + min-height: calc(var(--line-height) * 3); +} + +.min-lines-4 { + min-height: calc(var(--line-height) * 4); +} diff --git a/packages/atomic/src/components/common/expandable-text/expandable-text.tsx b/packages/atomic/src/components/common/expandable-text/expandable-text.tsx new file mode 100644 index 00000000000..707120cc8eb --- /dev/null +++ b/packages/atomic/src/components/common/expandable-text/expandable-text.tsx @@ -0,0 +1,92 @@ +import {FunctionalComponent, h} from '@stencil/core'; +import MinusIcon from '../../../images/minus.svg'; +import PlusIcon from '../../../images/plus.svg'; +import {Button} from '../button'; + +export type TruncateAfter = 'none' | '1' | '2' | '3' | '4'; + +interface ExpandableTextProps { + isExpanded: boolean; + isTruncated: boolean; + isCollapsible?: boolean; + truncateAfter: TruncateAfter; + onToggleExpand: (e: MouseEvent | undefined) => void; + showMoreLabel: string; + showLessLabel: string; +} + +const getLineClampClass = (truncateAfter: TruncateAfter) => { + const lineClampMap: Record = { + none: 'line-clamp-none', + 1: 'line-clamp-1', + 2: 'line-clamp-2', + 3: 'line-clamp-3', + 4: 'line-clamp-4', + }; + return lineClampMap[truncateAfter] || 'line-clamp-2'; +}; + +const renderShowHideButton = ( + isExpanded: boolean, + isTruncated: boolean, + isCollapsible: boolean, + onToggleExpand: (e?: MouseEvent) => void, + showMoreLabel: string, + showLessLabel: string +) => { + let buttonClass = 'expandable-text-button p-1 text-xs'; + if (!isTruncated && !isExpanded) { + buttonClass += ' invisible'; + } else if (!isCollapsible && !isTruncated && isExpanded) { + buttonClass += ' hidden'; + } + + const label = isExpanded ? showLessLabel : showMoreLabel; + + return ( + + ); +}; + +export const ExpandableText: FunctionalComponent = ( + { + isExpanded, + isTruncated, + truncateAfter, + onToggleExpand, + showMoreLabel, + showLessLabel, + isCollapsible = false, + }, + children +) => { + return ( +
    +
    + {children} +
    + {renderShowHideButton( + isExpanded, + isTruncated, + isCollapsible, + onToggleExpand, + showMoreLabel, + showLessLabel + )} +
    + ); +}; diff --git a/packages/atomic/src/components/common/generated-answer/styles/generated-answer.pcss b/packages/atomic/src/components/common/generated-answer/styles/generated-answer.pcss index 04333a673db..572fb16b564 100644 --- a/packages/atomic/src/components/common/generated-answer/styles/generated-answer.pcss +++ b/packages/atomic/src/components/common/generated-answer/styles/generated-answer.pcss @@ -71,9 +71,13 @@ } } +/** + * @prop --atomic-crga-collapsed-height: The maximum height of the collapsed generated answer container. + */ [part='generated-container'] { &.answer-collapsed { - @apply relative max-h-64 overflow-hidden content-['']; + @apply relative overflow-hidden content-['']; + max-height: var(--atomic-crga-collapsed-height, 16rem); .feedback-buttons { @apply hidden; @@ -81,7 +85,10 @@ } &.answer-collapsed:before { @apply absolute left-0 top-0 h-full w-full content-['']; - background: linear-gradient(transparent 11.25rem, var(--atomic-background)); + background: linear-gradient( + transparent calc(var(--atomic-crga-collapsed-height, 16rem) * 0.7), + var(--atomic-background) + ); } } diff --git a/packages/atomic/src/components/common/item-list/styles/mixins.pcss b/packages/atomic/src/components/common/item-list/styles/mixins.pcss index f539afa53f1..e4f140bfaee 100644 --- a/packages/atomic/src/components/common/item-list/styles/mixins.pcss +++ b/packages/atomic/src/components/common/item-list/styles/mixins.pcss @@ -211,6 +211,9 @@ grid-template-columns: repeat(3, 1fr); } @media (min-width: 1024px) { + grid-template-columns: repeat(3, 1fr); + } + @media (min-width: 1280px) { grid-template-columns: repeat(4, 1fr); } } diff --git a/packages/atomic/src/components/common/tabs/tab-bar.tsx b/packages/atomic/src/components/common/tabs/tab-bar.tsx index b64c82ddb03..1f8878e2dfe 100644 --- a/packages/atomic/src/components/common/tabs/tab-bar.tsx +++ b/packages/atomic/src/components/common/tabs/tab-bar.tsx @@ -148,6 +148,7 @@ export class TabBar { style="text-transparent" class="truncate rounded px-4 py-2 font-semibold" ariaLabel={tab.label} + title={tab.label} onClick={() => { tab.select(); this.tabPopover?.togglePopover(); diff --git a/packages/atomic/src/components/common/template-system/cell-desktop.pcss b/packages/atomic/src/components/common/template-system/cell-desktop.pcss index ec9325b8057..0e7b2a7c26c 100644 --- a/packages/atomic/src/components/common/template-system/cell-desktop.pcss +++ b/packages/atomic/src/components/common/template-system/cell-desktop.pcss @@ -4,11 +4,11 @@ grid-template-areas: 'badges' 'visual' - 'children' 'title' 'title-metadata' 'emphasized' 'excerpt' + 'children' 'bottom-metadata' 'actions'; grid-template-columns: 100%; diff --git a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts index a548b58bf33..04192588305 100644 --- a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts +++ b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts @@ -25,6 +25,7 @@ export interface AtomicInsightStoreData extends AtomicCommonStoreData { dateFacets: FacetStore>; categoryFacets: FacetStore; mobileBreakpoint: string; + currentQuickviewPosition: number; } export interface FacetInfoMap { @@ -51,6 +52,7 @@ export function createAtomicInsightStore(): AtomicInsightStore { fieldsToInclude: [], facetElements: [], mobileBreakpoint: DEFAULT_MOBILE_BREAKPOINT, + currentQuickviewPosition: -1, }); return { ...commonStore, diff --git a/packages/atomic/src/components/insight/atomic-insight-layout/insight-layout.ts b/packages/atomic/src/components/insight/atomic-insight-layout/insight-layout.ts index ebf4219c070..5fb8d2cd6b1 100644 --- a/packages/atomic/src/components/insight/atomic-insight-layout/insight-layout.ts +++ b/packages/atomic/src/components/insight/atomic-insight-layout/insight-layout.ts @@ -17,6 +17,10 @@ const smartSnippetSelectors = [ ]; const generatedAnswerSelector = 'atomic-insight-generated-answer'; +export function makeDesktopQuery(mobileBreakpoint: string) { + return `only screen and (min-width: ${mobileBreakpoint})`; +} + export function buildInsightLayout(element: HTMLElement, widget: boolean) { const id = element.id; const layoutSelector = `atomic-insight-layout#${id}`; diff --git a/packages/atomic/src/components/insight/atomic-insight-result-action-bar/atomic-insight-result-action-bar.pcss b/packages/atomic/src/components/insight/atomic-insight-result-action-bar/atomic-insight-result-action-bar.pcss index f2707d8b8e0..688b2b20384 100644 --- a/packages/atomic/src/components/insight/atomic-insight-result-action-bar/atomic-insight-result-action-bar.pcss +++ b/packages/atomic/src/components/insight/atomic-insight-result-action-bar/atomic-insight-result-action-bar.pcss @@ -3,7 +3,7 @@ atomic-insight-result-action-bar { @apply invisible absolute right-6 top-[-1rem] flex; - > atomic-insight-result-action:not(:last-child) { + > :not(:last-child) { button { @apply rounded-r-none border-r-0; &:hover { @@ -11,7 +11,7 @@ atomic-insight-result-action-bar { } } } - > atomic-insight-result-action:not(:first-child) { + > :not(:first-child) { button { @apply rounded-bl-none rounded-tl-none; } diff --git a/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.pcss b/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.pcss new file mode 100644 index 00000000000..c76e98dcfbd --- /dev/null +++ b/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.pcss @@ -0,0 +1 @@ +@import '../../../global/global.pcss'; diff --git a/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.tsx b/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.tsx new file mode 100644 index 00000000000..883bc284a5b --- /dev/null +++ b/packages/atomic/src/components/insight/atomic-insight-result-quickview-action/atomic-insight-result-quickview-action.tsx @@ -0,0 +1,158 @@ +import {Schema, StringValue} from '@coveo/bueno'; +import { + buildQuickview, + QuickviewState, + Quickview, + Result, +} from '@coveo/headless'; +import {Component, Listen, Prop, State, h, Element} from '@stencil/core'; +import QuickviewIcon from '../../../images/preview.svg'; +import { + AriaLiveRegion, + FocusTargetController, +} from '../../../utils/accessibility-utils'; +import { + BindStateToController, + InitializableComponent, + InitializeBindings, +} from '../../../utils/initialization-utils'; +import {IconButton} from '../../common/iconButton'; +import {Bindings} from '../../search/atomic-search-interface/atomic-search-interface'; +import {ResultContext} from '../../search/result-template-components/result-template-decorators'; + +/** + * @internal + */ +@Component({ + tag: 'atomic-insight-result-quickview-action', + styleUrl: 'atomic-insight-result-quickview-action.pcss', +}) +export class AtomicInsightResultQuickviewAction + implements InitializableComponent +{ + @InitializeBindings() public bindings!: Bindings; + @ResultContext() private result!: Result; + + private buttonFocusTarget?: FocusTargetController; + + @Element() host!: HTMLElement; + @State() public error!: Error; + + public quickview!: Quickview; + + @BindStateToController('quickview') + @State() + public quickviewState!: QuickviewState; + + /** + * The `sandbox` attribute to apply to the quickview iframe. + * + * The quickview is loaded inside an iframe with a [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox) attribute for security reasons. + * + * This attribute exists primarily to protect against potential XSS attacks that could originate from the document being displayed. + * + * By default, the sandbox attributes are: `allow-popups allow-top-navigation allow-same-origin`. + * + * `allow-same-origin` is not optional, and must always be included in the list of allowed capabilities for the component to function properly. + */ + @Prop() + public sandbox = 'allow-popups allow-top-navigation allow-same-origin'; + + @AriaLiveRegion('quickview') + protected quickviewAriaMessage!: string; + + @Listen('atomic/quickview/next', {target: 'body'}) + public onNextQuickview(evt: Event) { + evt.stopImmediatePropagation(); + this.quickview.next(); + } + + @Listen('atomic/quickview/previous', {target: 'body'}) + public onPreviousQuickview(evt: Event) { + evt.stopImmediatePropagation(); + this.quickview.previous(); + } + + private quickviewModalRef?: HTMLAtomicQuickviewModalElement; + + public get focusTarget() { + if (!this.buttonFocusTarget) { + this.buttonFocusTarget = new FocusTargetController(this); + } + return this.buttonFocusTarget; + } + + public initialize() { + this.quickview = buildQuickview(this.bindings.engine, { + options: {result: this.result}, + }); + new Schema({ + sandbox: new StringValue({ + required: true, + regex: /allow-same-origin/, + }), + }).validate({sandbox: this.sandbox}); + } + + private addQuickviewModalIfNeeded() { + if (this.quickviewModalRef) { + return; + } + + const quickviewModal = this.bindings.interfaceElement.querySelector( + 'atomic-quickview-modal' + ); + if (quickviewModal) { + this.quickviewModalRef = quickviewModal; + return; + } + this.quickviewModalRef = document.createElement('atomic-quickview-modal'); + this.quickviewModalRef.setAttribute('sandbox', this.sandbox); + this.bindings.interfaceElement.appendChild(this.quickviewModalRef); + } + + private updateModalContent() { + if (this.quickviewModalRef && this.quickview.state.content) { + this.quickviewModalRef.content = this.quickview.state.content; + this.quickviewModalRef.result = this.result; + this.quickviewModalRef.total = this.quickviewState.totalResults; + this.quickviewModalRef.current = this.quickviewState.currentResult; + this.quickviewModalRef.modalCloseCallback = () => + this.focusTarget.focus(); + + this.quickviewAriaMessage = this.quickviewState.isLoading + ? this.bindings.i18n.t('quickview-loading') + : this.bindings.i18n.t('quickview-loaded', { + first: this.quickviewState.currentResult, + last: this.quickviewState.totalResults, + title: this.result.title, + }); + } + } + + private onClick(event?: MouseEvent) { + event?.stopPropagation(); + this.quickview.fetchResultContent(); + } + + private get shouldRenderQuickview() { + return this.quickviewState.resultHasPreview; + } + + public render() { + this.addQuickviewModalIfNeeded(); + this.updateModalContent(); + if (this.shouldRenderQuickview) { + return ( + this.onClick()} + /> + ); + } + } +} diff --git a/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx b/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx index d6cf903aa12..88430ae7d92 100644 --- a/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx +++ b/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx @@ -95,6 +95,7 @@ export class AtomicInsightTab part="tab" class={buttonClasses.join(' ')} ariaLabel={this.bindings.i18n.t('tab-search', {label: this.label})} + title={this.label} ariaPressed={`${this.tabState.isActive}`} onClick={() => this.tab.select()} > diff --git a/packages/atomic/src/components/insight/index.ts b/packages/atomic/src/components/insight/index.ts index 8e0aa8ef457..0febfc784fe 100644 --- a/packages/atomic/src/components/insight/index.ts +++ b/packages/atomic/src/components/insight/index.ts @@ -105,4 +105,6 @@ export { UserSession as InsightUserSession, UserAction as InsightUserAction, buildUserActions as buildInsightUserActions, + Quickview as InsightQuickview, + QuickviewState as InsightQuickviewState, } from '@coveo/headless/insight'; diff --git a/packages/atomic/src/components/insight/result-templates/atomic-insight-result-children-template/atomic-insight-result-children-template.tsx b/packages/atomic/src/components/insight/result-templates/atomic-insight-result-children-template/atomic-insight-result-children-template.tsx index ff7c66fc897..425c475f632 100644 --- a/packages/atomic/src/components/insight/result-templates/atomic-insight-result-children-template/atomic-insight-result-children-template.tsx +++ b/packages/atomic/src/components/insight/result-templates/atomic-insight-result-children-template/atomic-insight-result-children-template.tsx @@ -47,15 +47,20 @@ export class AtomicInsightResultChildrenTemplate { * * For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` */ - @MapProp({splitValues: true}) public mustMatch: Record = {}; + @Prop() @MapProp({splitValues: true}) public mustMatch: Record< + string, + string[] + > = {}; /** * The field and values that define which result items the condition must not be applied to. * * For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` */ - @MapProp({splitValues: true}) public mustNotMatch: Record = - {}; + @Prop() @MapProp({splitValues: true}) public mustNotMatch: Record< + string, + string[] + > = {}; public resultTemplateCommon: ResultTemplateCommon; diff --git a/packages/atomic/src/components/insight/result-templates/atomic-insight-result-template/atomic-insight-result-template.tsx b/packages/atomic/src/components/insight/result-templates/atomic-insight-result-template/atomic-insight-result-template.tsx index 83e43ac5b90..f416d666417 100644 --- a/packages/atomic/src/components/insight/result-templates/atomic-insight-result-template/atomic-insight-result-template.tsx +++ b/packages/atomic/src/components/insight/result-templates/atomic-insight-result-template/atomic-insight-result-template.tsx @@ -50,15 +50,20 @@ export class AtomicInsightResultTemplate { * * For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` */ - @MapProp({splitValues: true}) public mustMatch: Record = {}; + @Prop() @MapProp({splitValues: true}) public mustMatch: Record< + string, + string[] + > = {}; /** * The field and values that define which result items the condition must not be applied to. * * For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` */ - @MapProp({splitValues: true}) public mustNotMatch: Record = - {}; + @Prop() @MapProp({splitValues: true}) public mustNotMatch: Record< + string, + string[] + > = {}; constructor() { this.resultTemplateCommon = new ResultTemplateCommon({ diff --git a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx index 2ae8c6f1536..099b2e6b356 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx @@ -350,6 +350,8 @@ export class AtomicIPXRecsList implements InitializableComponent { this.imageSize ), content: this.itemTemplateProvider.getTemplateContent(recommendation), + linkContent: + this.itemTemplateProvider.getLinkTemplateContent(recommendation), store: this.bindings.store, density: this.density, display: this.display, diff --git a/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx b/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx index e1f20f7325c..d410d934ce6 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx @@ -90,6 +90,7 @@ export class AtomicIPXTab implements InitializableComponent { part="tab" class={buttonClasses.join(' ')} ariaLabel={this.bindings.i18n.t('tab-search', {label: this.label})} + title={this.label} ariaPressed={`${this.tabState.isActive}`} onClick={() => this.tab.select()} > diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx index a131f53d691..c69e927dc1b 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx @@ -326,6 +326,8 @@ export class AtomicRecsList implements InitializableComponent { this.imageSize ), content: this.itemTemplateProvider.getTemplateContent(recommendation), + linkContent: + this.itemTemplateProvider.getLinkTemplateContent(recommendation), store: this.bindings.store, density: this.density, display: this.display, diff --git a/packages/atomic/src/components/recommendations/atomic-recs-result-template/atomic-recs-result-template.tsx b/packages/atomic/src/components/recommendations/atomic-recs-result-template/atomic-recs-result-template.tsx index 310f8e8b654..41b56daff5b 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-result-template/atomic-recs-result-template.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-result-template/atomic-recs-result-template.tsx @@ -56,15 +56,20 @@ export class AtomicRecsResultTemplate { * * For example, a template with the following attribute only applies to result items whose `filetype` is `lithiummessage` or `YouTubePlaylist`: `must-match-filetype="lithiummessage,YouTubePlaylist"` */ - @MapProp({splitValues: true}) public mustMatch: Record = {}; + @Prop() @MapProp({splitValues: true}) public mustMatch: Record< + string, + string[] + > = {}; /** * The field and values that define which result items the condition must not be applied to. * * For example, a template with the following attribute only applies to result items whose `filetype` is not `lithiummessage`: `must-not-match-filetype="lithiummessage"` */ - @MapProp({splitValues: true}) public mustNotMatch: Record = - {}; + @Prop() @MapProp({splitValues: true}) public mustNotMatch: Record< + string, + string[] + > = {}; constructor() { this.resultTemplateCommon = new ResultTemplateCommon({ diff --git a/packages/atomic/src/components/recommendations/atomic-recs-result/atomic-recs-result.tsx b/packages/atomic/src/components/recommendations/atomic-recs-result/atomic-recs-result.tsx index 8cfbec370f8..1b0f4136a3f 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-result/atomic-recs-result.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-result/atomic-recs-result.tsx @@ -1,5 +1,6 @@ import {Component, h, Prop, Element, Listen, Host} from '@stencil/core'; import {RecsInteractiveResult, RecsResult} from '..'; +import {parentNodeToString} from '../../../utils/dom-utils'; import {applyFocusVisiblePolyfill} from '../../../utils/initialization-utils'; import { InteractiveItemContextEvent, @@ -29,6 +30,7 @@ import {AtomicRecsStore} from '../atomic-recs-interface/store'; export class AtomicRecsResult { private layout!: ItemLayout; private resultRootRef?: HTMLElement; + private linkContainerRef?: HTMLElement; private executedRenderingFunctionOnce = false; @Element() host!: HTMLElement; @@ -37,6 +39,13 @@ export class AtomicRecsResult { */ @Prop() stopPropagation?: boolean; + /** + * The result link to use when the result is clicked in a grid layout. + * + * @default - An `atomic-result-link` without any customization. + */ + @Prop() linkContent: ParentNode = new DocumentFragment(); + /** * The result item. */ @@ -94,6 +103,18 @@ export class AtomicRecsResult { */ @Prop() renderingFunction: ItemRenderingFunction; + @Listen('click') + public handleClick(event: MouseEvent) { + if (this.stopPropagation) { + event.stopPropagation(); + } + this.host + .shadowRoot!.querySelector( + '.link-container > atomic-result-link a:not([slot])' + ) + ?.click(); + } + @Listen('atomic/resolveResult') public resolveResult(event: ItemContextEvent) { event.preventDefault(); @@ -133,11 +154,12 @@ export class AtomicRecsResult { } private getContentHTML() { - return Array.from(this.content!.children) - .map((child) => child.outerHTML) - .join(''); + return parentNodeToString(this.content!); } + private getLinkHTML() { + return parentNodeToString(this.linkContent); + } private get isCustomRenderFunctionMode() { return this.renderingFunction !== undefined; } @@ -158,6 +180,10 @@ export class AtomicRecsResult { class="result-root" ref={(ref) => (this.resultRootRef = ref)} > + ); } @@ -171,6 +197,7 @@ export class AtomicRecsResult { .join(' ')}`} innerHTML={this.getContentHTML()} > + ); } @@ -186,7 +213,8 @@ export class AtomicRecsResult { if (this.shouldExecuteRenderFunction()) { const customRenderOutputAsString = this.renderingFunction!( this.result, - this.resultRootRef! + this.resultRootRef!, + this.linkContainerRef! ); this.resultRootRef!.className += ` ${this.layout diff --git a/packages/atomic/src/components/search/atomic-generated-answer/atomic-generated-answer.tsx b/packages/atomic/src/components/search/atomic-generated-answer/atomic-generated-answer.tsx index 6882feee8ef..a659237a4b7 100644 --- a/packages/atomic/src/components/search/atomic-generated-answer/atomic-generated-answer.tsx +++ b/packages/atomic/src/components/search/atomic-generated-answer/atomic-generated-answer.tsx @@ -105,7 +105,7 @@ export class AtomicGeneratedAnswer implements InitializableComponent { @Prop() withToggle?: boolean; /** - * Whether to allow the answer to be collapsed when the text is taller than 250px. + * Whether to allow the answer to be collapsed when the text is taller than the specified `--atomic-crga-collapsed-height` value (16rem by default). * @default false */ @Prop() collapsible?: boolean; diff --git a/packages/atomic/src/components/search/facets/atomic-numeric-facet/atomic-numeric-facet.new.stories.tsx b/packages/atomic/src/components/search/facets/atomic-numeric-facet/atomic-numeric-facet.new.stories.tsx index 2b4149e7801..dde2a58afb8 100644 --- a/packages/atomic/src/components/search/facets/atomic-numeric-facet/atomic-numeric-facet.new.stories.tsx +++ b/packages/atomic/src/components/search/facets/atomic-numeric-facet/atomic-numeric-facet.new.stories.tsx @@ -31,6 +31,6 @@ export const Default: Story = { name: 'atomic-numeric-facet', decorators: [facetDecorator], args: { - field: 'ytviewcount', + 'attributes-field': 'ytviewcount', }, }; diff --git a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/atomic-folded-result-list.new.stories.tsx b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/atomic-folded-result-list.new.stories.tsx index a8acd775546..2a7b167ac3c 100644 --- a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/atomic-folded-result-list.new.stories.tsx +++ b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/atomic-folded-result-list.new.stories.tsx @@ -133,7 +133,6 @@ const meta: Meta = { component: 'atomic-folded-result-list', title: 'Atomic/FoldedResultList', id: 'atomic-folded-result-list', - render: renderComponent, decorators: [decorator], parameters, diff --git a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/atomic-folded-result-list.e2e.ts b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/atomic-folded-result-list.e2e.ts index a474654e4f9..8199a604bca 100644 --- a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/atomic-folded-result-list.e2e.ts +++ b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/atomic-folded-result-list.e2e.ts @@ -1,10 +1,94 @@ import {test, expect} from './fixture'; -// TODO: KIT-3546 - Make this test pass -test.describe('When no child results', () => { - test.fixme('should show a "no results" label', async ({foldedResultList}) => { +test.describe('when more results are NOT available & there are NO result children', () => { + test.beforeEach(async ({foldedResultList}) => { await foldedResultList.load({story: 'with-no-result-children'}); - await foldedResultList.loadAllResultsButton.click(); + }); + + test('should show a "no results" label', async ({foldedResultList}) => { await expect(foldedResultList.noResultsLabel.first()).toBeVisible(); }); + + test('should NOT show the "load all results" button', async ({ + foldedResultList, + }) => { + await expect( + foldedResultList.loadAllResultsButton.first() + ).not.toBeVisible(); + }); + + test('should NOT show result children', async ({foldedResultList}) => { + await expect(foldedResultList.resultChildren.first()).not.toBeVisible(); + }); +}); + +test.describe('when more results are NOT available & there are result children', () => { + test.beforeEach(async ({foldedResultList}) => { + await foldedResultList.withATotalNumberOfChildResults(1); + await foldedResultList.load(); + }); + + test('should show result children', async ({foldedResultList}) => { + await expect(foldedResultList.resultChildren.first()).toBeVisible(); + }); + + test('should NOT show a "no results" label', async ({foldedResultList}) => { + await expect(foldedResultList.noResultsLabel.first()).not.toBeVisible(); + }); + + test('should NOT show "load all results" button', async ({ + foldedResultList, + }) => { + await expect( + foldedResultList.loadAllResultsButton.first() + ).not.toBeVisible(); + }); +}); + +test.describe('when more results are available & there are result children', () => { + test.beforeEach(async ({foldedResultList}) => { + await foldedResultList.load(); + }); + + test('should show the "load all results" button', async ({ + foldedResultList, + }) => { + await expect(foldedResultList.loadAllResultsButton.first()).toBeVisible(); + }); + + test('should show the "Collapse results" button after loading all results', async ({ + foldedResultList, + }) => { + await foldedResultList.loadAllResultsButton.first().click(); + await expect(foldedResultList.collapseResultsButton.first()).toBeVisible(); + }); + + test('should show result children', async ({foldedResultList}) => { + await expect(foldedResultList.resultChildren.first()).toBeVisible(); + }); + + test('should NOT show the "no results" label', async ({foldedResultList}) => { + await expect(foldedResultList.noResultsLabel.first()).not.toBeVisible(); + }); +}); + +test.describe('when more results are available & there are NO result children', () => { + test.beforeEach(async ({foldedResultList}) => { + await foldedResultList.withATotalNumberOfChildResults(10); + await foldedResultList.load({story: 'with-no-result-children'}); + }); + + test('should show the "load all results" button', async ({ + foldedResultList, + }) => { + await expect(foldedResultList.loadAllResultsButton.first()).toBeVisible(); + }); + + test('should NOT show result children', async ({foldedResultList}) => { + await expect(foldedResultList.resultChildren.first()).not.toBeVisible(); + }); + + test('should NOT show the "no results" label', async ({foldedResultList}) => { + await expect(foldedResultList.noResultsLabel.first()).not.toBeVisible(); + }); }); diff --git a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/page-object.ts b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/page-object.ts index 870788e58de..7e580b531c2 100644 --- a/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/page-object.ts +++ b/packages/atomic/src/components/search/result-lists/atomic-folded-result-list/e2e/page-object.ts @@ -13,4 +13,28 @@ export class AtomicFoldedResultListPageObject extends BasePageObject<'atomic-fol get loadAllResultsButton() { return this.page.getByRole('button', {name: 'Load all results'}); } + + get collapseResultsButton() { + return this.page.getByRole('button', {name: 'Collapse results'}); + } + + get resultChildren() { + return this.page.locator('[part="children-root"]'); + } + + async withATotalNumberOfChildResults(total: number) { + await this.page.route( + '**/search/v2?organizationId=searchuisamples', + async (route) => { + const response = await route.fetch(); + const body = await response.json(); + body.results[0].totalNumberOfChildResults = total; + + await route.fulfill({ + response, + json: body, + }); + } + ); + } } diff --git a/packages/atomic/src/components/search/result-lists/atomic-result-children/atomic-result-children.tsx b/packages/atomic/src/components/search/result-lists/atomic-result-children/atomic-result-children.tsx index 581b7882080..e4a81e5e9af 100644 --- a/packages/atomic/src/components/search/result-lists/atomic-result-children/atomic-result-children.tsx +++ b/packages/atomic/src/components/search/result-lists/atomic-result-children/atomic-result-children.tsx @@ -4,7 +4,15 @@ import { FoldedResultList, FoldedResultListState, } from '@coveo/headless'; -import {Component, Element, State, h, Listen, Prop} from '@stencil/core'; +import { + Component, + Element, + State, + h, + Listen, + Prop, + Fragment, +} from '@stencil/core'; import {buildCustomEvent} from '../../../../utils/event-utils'; import { InitializableComponent, @@ -163,6 +171,7 @@ export class AtomicResultChildren implements InitializableComponent { }); } private loadFullCollection() { + this.loadedFullCollection = true; this.host.dispatchEvent( buildCustomEvent('atomic/loadCollection', this.collection) ); @@ -177,6 +186,8 @@ export class AtomicResultChildren implements InitializableComponent { this.showInitialChildren = !this.showInitialChildren; }; + @State() private loadedFullCollection = false; + private renderCollection() { const collection = this.collection!; @@ -184,30 +195,38 @@ export class AtomicResultChildren implements InitializableComponent { ? this.initialChildren : collection.children; + const showShouldButtons = + this.loadedFullCollection || collection.moreResultsAvailable; + return ( - 0} - numberOfChildren={collection.children.length} - density={this.displayConfig.density} - imageSize={this.imageSize || this.displayConfig.imageSize} - noResultText={this.bindings.i18n.t(this.noResultText)} - > - + {showShouldButtons && ( + this.loadFullCollection()} + showInitialChildren={this.showInitialChildren} + toggleShowInitialChildren={this.toggleShowInitialChildren} + loadAllResults={this.bindings.i18n.t('load-all-results')} + collapseResults={this.bindings.i18n.t('collapse-results')} + /> + )} + + this.loadFullCollection()} - showInitialChildren={this.showInitialChildren} - toggleShowInitialChildren={this.toggleShowInitialChildren} - loadAllResults={this.bindings.i18n.t('load-all-results')} - collapseResults={this.bindings.i18n.t('collapse-results')} - > - 0}> - {children.map((child, i) => - this.renderChild(child, i === children.length - 1) - )} - - + hasChildren={collection.children.length > 0} + numberOfChildren={collection.children.length} + density={this.displayConfig.density} + imageSize={this.imageSize || this.displayConfig.imageSize} + noResultText={this.bindings.i18n.t(this.noResultText)} + > + 0}> + {children.map((child, i) => + this.renderChild(child, i === children.length - 1) + )} + + + ); } diff --git a/packages/atomic/src/components/search/result-template-components/atomic-field-condition/atomic-field-condition.tsx b/packages/atomic/src/components/search/result-template-components/atomic-field-condition/atomic-field-condition.tsx index da60557c410..fb0801b067b 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-field-condition/atomic-field-condition.tsx +++ b/packages/atomic/src/components/search/result-template-components/atomic-field-condition/atomic-field-condition.tsx @@ -34,9 +34,19 @@ export class AtomicFieldCondition { */ @Prop({reflect: true}) ifNotDefined?: string; - @MapProp({splitValues: true}) mustMatch: Record = {}; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) mustMatch: Record = + {}; - @MapProp({splitValues: true}) mustNotMatch: Record = {}; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) mustNotMatch: Record = + {}; private conditions: ResultTemplateCondition[] = []; private shouldBeRemoved = false; diff --git a/packages/atomic/src/components/search/result-template-components/atomic-result-localized-text/atomic-result-localized-text.ts b/packages/atomic/src/components/search/result-template-components/atomic-result-localized-text/atomic-result-localized-text.ts index 38e244e673e..63d002e01e2 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-result-localized-text/atomic-result-localized-text.ts +++ b/packages/atomic/src/components/search/result-template-components/atomic-result-localized-text/atomic-result-localized-text.ts @@ -41,7 +41,7 @@ export class AtomicResultLocalizedText implements InitializableComponent { /** * The field value to dynamically evaluate. */ - @MapProp() field: Record = {}; + @Prop() @MapProp() field: Record = {}; /** * The numerical field value used to determine whether to use the singular or plural value of a translation. * */ diff --git a/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx b/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx index ca3ae3e4525..3a6f3578c79 100644 --- a/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx +++ b/packages/atomic/src/components/search/result-template-components/atomic-result-multi-value-text/atomic-result-multi-value-text.tsx @@ -118,13 +118,7 @@ export class AtomicResultMultiText { } private getNumberOfValuesToDisplay(values: string[]) { - if (values.length <= this.maxValuesToDisplay) { - return values.length; - } - if (this.maxValuesToDisplay < 2) { - return this.maxValuesToDisplay; - } - return Math.min(values.length - 2, this.maxValuesToDisplay); + return Math.min(values.length, this.maxValuesToDisplay); } private renderValue(value: string) { diff --git a/packages/atomic/src/components/search/result-templates/atomic-result-children-template/atomic-result-children-template.tsx b/packages/atomic/src/components/search/result-templates/atomic-result-children-template/atomic-result-children-template.tsx index 75e8e935d45..5fa568f7a26 100644 --- a/packages/atomic/src/components/search/result-templates/atomic-result-children-template/atomic-result-children-template.tsx +++ b/packages/atomic/src/components/search/result-templates/atomic-result-children-template/atomic-result-children-template.tsx @@ -31,10 +31,23 @@ export class AtomicResultChildrenTemplate { */ @Prop() public conditions: ResultTemplateCondition[] = []; - @MapProp({splitValues: true}) public mustMatch: Record = {}; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) public mustMatch: Record< + string, + string[] + > = {}; - @MapProp({splitValues: true}) public mustNotMatch: Record = - {}; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) public mustNotMatch: Record< + string, + string[] + > = {}; public resultTemplateCommon: ResultTemplateCommon; diff --git a/packages/atomic/src/components/search/result-templates/atomic-result-template/atomic-result-template.tsx b/packages/atomic/src/components/search/result-templates/atomic-result-template/atomic-result-template.tsx index de5677c1b5f..90061531db0 100644 --- a/packages/atomic/src/components/search/result-templates/atomic-result-template/atomic-result-template.tsx +++ b/packages/atomic/src/components/search/result-templates/atomic-result-template/atomic-result-template.tsx @@ -36,10 +36,23 @@ export class AtomicResultTemplate { */ @Prop() public conditions: ResultTemplateCondition[] = []; - @MapProp({splitValues: true}) public mustMatch: Record = {}; + /** + * Verifies whether the specified fields match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) public mustMatch: Record< + string, + string[] + > = {}; - @MapProp({splitValues: true}) public mustNotMatch: Record = - {}; + /** + * Verifies whether the specified fields do not match the specified values. + * @type {Record} + */ + @Prop() @MapProp({splitValues: true}) public mustNotMatch: Record< + string, + string[] + > = {}; constructor() { this.resultTemplateCommon = new ResultTemplateCommon({ diff --git a/packages/atomic/src/pages/examples/commerce-website/cart.html b/packages/atomic/src/pages/examples/commerce-website/cart.html index 2c5185bf747..746f2c2edba 100644 --- a/packages/atomic/src/pages/examples/commerce-website/cart.html +++ b/packages/atomic/src/pages/examples/commerce-website/cart.html @@ -55,6 +55,7 @@

    Cart

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/homepage.html b/packages/atomic/src/pages/examples/commerce-website/homepage.html index 1c7ef5ed9f0..120d16e0eb8 100644 --- a/packages/atomic/src/pages/examples/commerce-website/homepage.html +++ b/packages/atomic/src/pages/examples/commerce-website/homepage.html @@ -59,6 +59,7 @@

    HOMEPAGE

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-pants.html b/packages/atomic/src/pages/examples/commerce-website/listing-pants.html index b11092c8962..3c27803906e 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-pants.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-pants.html @@ -52,6 +52,7 @@

    Pants

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html b/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html index b4a3e82f08d..030f6052c4d 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-surf-accessories.html @@ -52,6 +52,7 @@

    Surf accessories

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/listing-towels.html b/packages/atomic/src/pages/examples/commerce-website/listing-towels.html index 05390d8f402..b79cc5f6657 100644 --- a/packages/atomic/src/pages/examples/commerce-website/listing-towels.html +++ b/packages/atomic/src/pages/examples/commerce-website/listing-towels.html @@ -52,6 +52,7 @@

    Towels

    + diff --git a/packages/atomic/src/pages/examples/commerce-website/search.html b/packages/atomic/src/pages/examples/commerce-website/search.html index 3420e6fd93d..30d48c178f4 100644 --- a/packages/atomic/src/pages/examples/commerce-website/search.html +++ b/packages/atomic/src/pages/examples/commerce-website/search.html @@ -47,6 +47,11 @@

    Search page

    + + + @@ -77,7 +82,20 @@

    Search page

    diff --git a/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css b/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css index ade5acd008e..ecd4ae08609 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css +++ b/packages/quantic/force-app/main/default/lwc/quanticRefineToggle/quanticRefineToggle.css @@ -21,6 +21,7 @@ width: 1.25rem; height: 1.25rem; font-weight: bold; + z-index: 1; } .refine-button { diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js index 9cf2bc7bb9c..14925e30a05 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js +++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/__tests__/quanticSearchBox.test.js @@ -1,14 +1,92 @@ +/* eslint-disable no-import-assign */ import QuanticSearchBox from 'c/quanticSearchBox'; // @ts-ignore import {createElement} from 'lwc'; +import * as mockHeadlessLoader from 'c/quanticHeadlessLoader'; -describe('c-quantic-search-box', () => { - 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.mock('c/quanticHeadlessLoader'); + +let isInitialized = false; + +const exampleEngine = { + id: 'dummy engine', +}; + +const functionsMocks = { + buildSearchBox: jest.fn(() => ({ + state: {}, + subscribe: functionsMocks.subscribe, + })), + loadQuerySuggestActions: jest.fn(() => {}), + subscribe: jest.fn((cb) => { + cb(); + return functionsMocks.unsubscribe; + }), + unsubscribe: jest.fn(() => {}), +}; + +const defaultOptions = { + engineId: exampleEngine.id, + placeholder: null, + withoutSubmitButton: false, + numberOfSuggestions: 7, + textarea: false, + disableRecentQueries: false, + keepFiltersOnSearch: false, +}; + +function createTestComponent(options = defaultOptions) { + prepareHeadlessState(); + + const element = createElement('c-quantic-search-box', { + is: QuanticSearchBox, + }); + for (const [key, value] of Object.entries(options)) { + element[key] = value; + } + document.body.appendChild(element); + return element; +} + +function prepareHeadlessState() { + // @ts-ignore + mockHeadlessLoader.getHeadlessBundle = () => { + return { + buildSearchBox: functionsMocks.buildSearchBox, + loadQuerySuggestActions: functionsMocks.loadQuerySuggestActions, + }; + }; +} + +// 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 QuanticSearchBox && !isInitialized) { + isInitialized = true; + initialize(exampleEngine); } + }; +} + +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-search-box', () => { + beforeAll(() => { + mockSuccessfulHeadlessInitialization(); + }); afterEach(() => { cleanup(); @@ -19,4 +97,46 @@ describe('c-quantic-search-box', () => { createElement('c-quantic-search-box', {is: QuanticSearchBox}) ).not.toThrow(); }); + + describe('controller initialization', () => { + it('should subscribe to the headless state changes', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); + }); + + describe('when keepFiltersOnSearch is false (default)', () => { + it('should properly initialize the controller with clear filters enabled', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1); + expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith( + exampleEngine, + expect.objectContaining({ + options: expect.objectContaining({clearFilters: true}), + }) + ); + }); + }); + + describe('when keepFiltersOnSearch is true', () => { + it('should properly initialize the controller with clear filters disabled', async () => { + createTestComponent({ + ...defaultOptions, + keepFiltersOnSearch: true, + }); + await flushPromises(); + + expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1); + expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith( + exampleEngine, + expect.objectContaining({ + options: expect.objectContaining({clearFilters: false}), + }) + ); + }); + }); + }); }); diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js index 6287c7970c8..94733d28516 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js +++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBox/quanticSearchBox.js @@ -66,6 +66,13 @@ export default class QuanticSearchBox extends LightningElement { * @defaultValue false */ @api disableRecentQueries = false; + /** + * Whether to keep all active query filters when the end user submits a new query from the search box. + * @api + * @type {boolean} + * @defaultValue false + */ + @api keepFiltersOnSearch = false; /** @type {SearchBoxState} */ @track state; @@ -100,6 +107,7 @@ export default class QuanticSearchBox extends LightningElement { close: '', }, }, + clearFilters: !this.keepFiltersOnSearch, }, }); diff --git a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css index bad3636a845..8b1297467e9 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css +++ b/packages/quantic/force-app/main/default/lwc/quanticSearchBoxInput/templates/expandableSearchBoxInput.css @@ -37,6 +37,7 @@ textarea.searchbox__input { border: none; box-shadow: none; overflow-x: clip; + overflow-y: hidden; margin-right: 0.8rem; } diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js new file mode 100644 index 00000000000..a41ba41be23 --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/__tests__/quanticStandaloneSearchBox.test.js @@ -0,0 +1,190 @@ +/* eslint-disable no-import-assign */ +import QuanticStandaloneSearchBox from 'c/quanticStandaloneSearchBox'; +// @ts-ignore +import {createElement} from 'lwc'; +import * as mockHeadlessLoader from 'c/quanticHeadlessLoader'; +import {CurrentPageReference} from 'lightning/navigation'; +import getHeadlessConfiguration from '@salesforce/apex/HeadlessController.getHeadlessConfiguration'; + +const nonStandaloneURL = 'https://www.example.com/global-search/%40uri'; +const defaultHeadlessConfiguration = JSON.stringify({ + organization: 'testOrgId', + accessToken: 'testAccessToken', +}); + +jest.mock('c/quanticHeadlessLoader'); + +jest.mock( + '@salesforce/apex/HeadlessController.getHeadlessConfiguration', + () => ({ + default: jest.fn(), + }), + {virtual: true} +); + +mockHeadlessLoader.loadDependencies = () => + new Promise((resolve) => { + resolve(); + }); + +let isInitialized = false; + +const exampleEngine = { + id: 'engineId', +}; + +const functionsMocks = { + buildStandaloneSearchBox: jest.fn(() => ({ + state: {}, + subscribe: functionsMocks.subscribe, + })), + subscribe: jest.fn((cb) => { + cb(); + return functionsMocks.unsubscribe; + }), + unsubscribe: jest.fn(() => {}), +}; + +const defaultOptions = { + engineId: exampleEngine.id, + placeholder: null, + withoutSubmitButton: false, + numberOfSuggestions: 7, + textarea: false, + disableRecentQueries: false, + keepFiltersOnSearch: false, + redirectUrl: '/global-search/%40uri', +}; + +function createTestComponent(options = defaultOptions) { + prepareHeadlessState(); + const element = createElement('c-quantic-standalone-search-box', { + is: QuanticStandaloneSearchBox, + }); + for (const [key, value] of Object.entries(options)) { + element[key] = value; + } + document.body.appendChild(element); + return element; +} + +function prepareHeadlessState() { + // @ts-ignore + global.CoveoHeadless = { + buildStandaloneSearchBox: functionsMocks.buildStandaloneSearchBox, + }; +} + +// 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 QuanticStandaloneSearchBox && !isInitialized) { + isInitialized = true; + initialize(exampleEngine); + } + }; +} + +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-standalone-search-box', () => { + beforeEach(() => { + getHeadlessConfiguration.mockResolvedValue(defaultHeadlessConfiguration); + mockSuccessfulHeadlessInitialization(); + }); + + afterEach(() => { + cleanup(); + }); + + it('construct itself without throwing', () => { + expect(() => createTestComponent()).not.toThrow(); + }); + + describe('controller initialization', () => { + it('should subscribe to the headless state changes', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); + }); + + describe('when the current page reference changes', () => { + beforeAll(() => { + // This is needed to mock the window.location.href property to test the keepFiltersOnSearch property in the quanticSearchBox. + // https://stackoverflow.com/questions/54021037/how-to-mock-window-location-href-with-jest-vuejs + Object.defineProperty(window, 'location', { + writable: true, + value: {href: nonStandaloneURL}, + }); + }); + + it('should properly pass the keepFiltersOnSearch property to the quanticSearchBox', async () => { + const element = createTestComponent({ + ...defaultOptions, + keepFiltersOnSearch: false, + }); + // eslint-disable-next-line @lwc/lwc/no-unexpected-wire-adapter-usages + CurrentPageReference.emit({url: nonStandaloneURL}); + await flushPromises(); + + const searchBox = element.shadowRoot.querySelector( + 'c-quantic-search-box' + ); + + expect(searchBox).not.toBeNull(); + expect(searchBox.keepFiltersOnSearch).toEqual(false); + }); + }); + + describe('when keepFiltersOnSearch is false (default)', () => { + it('should properly initialize the controller with clear filters enabled', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes( + 1 + ); + expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith( + exampleEngine, + expect.objectContaining({ + options: expect.objectContaining({clearFilters: true}), + }) + ); + }); + }); + + describe('when keepFiltersOnSearch is true', () => { + it('should properly initialize the controller with clear filters disabled', async () => { + createTestComponent({ + ...defaultOptions, + keepFiltersOnSearch: true, + }); + await flushPromises(); + + expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes( + 1 + ); + expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith( + exampleEngine, + expect.objectContaining({ + options: expect.objectContaining({clearFilters: false}), + }) + ); + }); + }); + }); +}); diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js index 8d90e3748ba..80254aa7d36 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js +++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/quanticStandaloneSearchBox.js @@ -55,6 +55,13 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin( * @defaultValue 5 */ @api numberOfSuggestions = 5; + /** + * Whether to keep all active query filters when the end user submits a new query from the standalone search box. + * @api + * @type {boolean} + * @defaultValue false + */ + @api keepFiltersOnSearch = false; /** * The url of the search page to redirect to when a query is made. * The target search page should contain a `QuanticSearchInterface` with the same engine ID as the one specified for this component. @@ -171,6 +178,7 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin( close: '', }, }, + clearFilters: !this.keepFiltersOnSearch, redirectionUrl: 'http://placeholder.com', }, }); diff --git a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html index f1671df53a8..ae1180513fa 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html +++ b/packages/quantic/force-app/main/default/lwc/quanticStandaloneSearchBox/templates/standaloneSearchBox.html @@ -24,6 +24,7 @@ without-submit-button={withoutSubmitButton} number-of-suggestions={numberOfSuggestions} textarea={textarea} + keep-filters-on-search={keepFiltersOnSearch} > diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js b/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js index 491323c28ff..a3e87a080bc 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js +++ b/packages/quantic/force-app/main/default/lwc/quanticUserAction/__tests__/quanticUserAction.test.js @@ -49,6 +49,7 @@ const expectations = { viewAction: { iconName: 'utility:preview', iconClass: 'user-action__view-action-icon', + titleClass: 'user-action__title', }, }; @@ -364,15 +365,49 @@ describe('c-quantic-user-action', () => { expect(icon.classList.contains(iconClass)).toBe(true); }); - it('should properly display the action title', async () => { - const element = createTestComponent({action: exampleAction}); - await flushPromises(); + describe('when the contentIdKey of the action is clickable', () => { + it('should display the action title as a link', async () => { + const element = createTestComponent({ + action: { + ...exampleAction, + document: { + ...exampleAction.document, + contentIdKey: '@clickableuri', + }, + }, + }); + await flushPromises(); - const link = element.shadowRoot.querySelector(selectors.link); + const link = element.shadowRoot.querySelector(selectors.link); + + expect(link).not.toBeNull(); + expect(link.textContent).toBe(expectedTitle); + expect(link.href).toBe(expectedUrl); + }); + }); - expect(link).not.toBeNull(); - expect(link.textContent).toBe(expectedTitle); - expect(link.href).toBe(expectedUrl); + describe('when the contentIdKey of the action is not clickable', () => { + it('should display the action title as a text', async () => { + const element = createTestComponent({ + action: { + ...exampleAction, + document: { + ...exampleAction.document, + contentIdKey: '@sfid', + }, + }, + }); + await flushPromises(); + + const title = element.shadowRoot.querySelector(selectors.title); + const link = element.shadowRoot.querySelector(selectors.link); + const {titleClass} = expectations.viewAction; + + expect(link).toBeNull(); + expect(title).not.toBeNull(); + expect(title.textContent).toBe(expectedTitle); + expect(title.classList.contains(titleClass)).toBe(true); + }); }); it('should properly display the action details', async () => { diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js b/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js index 91a81486da5..1076c4aaeb9 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js +++ b/packages/quantic/force-app/main/default/lwc/quanticUserAction/quanticUserAction.js @@ -33,6 +33,7 @@ export default class QuanticUserAction extends LightningElement { ticketCreated, emptySearch, }; + clickableContentIdKeys = ['@clickableuri']; get iconName() { return icons[this.action?.actionType]; @@ -74,6 +75,10 @@ export default class QuanticUserAction extends LightningElement { return this.action?.document?.contentIdValue; } + get contentIdKey() { + return this.action?.document?.contentIdKey; + } + get iconClass() { switch (this.action?.actionType) { case 'TICKET_CREATION': @@ -97,7 +102,11 @@ export default class QuanticUserAction extends LightningElement { } render() { - if (this.action?.actionType === 'VIEW') return viewActionTemplate; + const viewEventCanBeDisplayedAsLink = this.clickableContentIdKeys.includes( + this.contentIdKey + ); + if (this.action?.actionType === 'VIEW' && viewEventCanBeDisplayedAsLink) + return viewActionTemplate; return actionTemplate; } } diff --git a/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js b/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js index 953633db3c3..e466c3f0c43 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js +++ b/packages/quantic/force-app/main/default/lwc/quanticUserActionsToggle/quanticUserActionsToggle.js @@ -77,8 +77,8 @@ export default class QuanticUserActionsToggle extends LightningElement { this.userActions = this.headless.buildUserActions(engine, { options: { ticketCreationDate: this.ticketCreationDateTime, - excludedCustomActions: this.excludedCustomActions?.length - ? this.excludedCustomActions + excludedCustomActions: Array.isArray(this.excludedCustomActions) + ? [...this.excludedCustomActions] : [], }, }); 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 diff --git a/packages/quantic/package.json b/packages/quantic/package.json index 82d83d510dd..005f987e9f8 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/quantic", - "version": "3.2.1", + "version": "3.3.0", "description": "A Salesforce Lightning Web Component (LWC) library for building modern UIs interfacing with the Coveo platform", "author": "coveo.com", "homepage": "https://coveo.com", @@ -40,18 +40,18 @@ "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" }, "dependencies": { - "@coveo/bueno": "1.0.1", - "@coveo/headless": "3.4.0", + "@coveo/bueno": "1.0.2", + "@coveo/headless": "3.5.0", "dompurify": "3.1.6", "marked": "12.0.2" }, "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "devDependencies": { "@ckeditor/jsdoc-plugins": "39.9.1", diff --git a/packages/rollup-plugin-replace-with-ast/LICENSE b/packages/rollup-plugin-replace-with-ast/LICENSE deleted file mode 100644 index 052e59b641a..00000000000 --- a/packages/rollup-plugin-replace-with-ast/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Coveo Solutions Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/packages/rollup-plugin-replace-with-ast/package.json b/packages/rollup-plugin-replace-with-ast/package.json deleted file mode 100644 index bad2213f1fb..00000000000 --- a/packages/rollup-plugin-replace-with-ast/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@coveo/rollup-plugin-replace-with-ast", - "repository": { - "type": "git", - "url": "git+https://github.com/coveo/ui-kit.git", - "directory": "packages/rollup-plugin-replace-with-ast" - }, - "private": true, - "main": "./dist/cjs/index.js", - "module": "./dist/es/index.js", - "exports": { - "types": "./types/index.d.ts", - "import": "./dist/es/index.js", - "default": "./dist/cjs/index.js" - }, - "engines": { - "node": "^20.9.0" - }, - "types": "./dist/definitions/index.d.ts", - "license": "Apache-2.0", - "version": "1.0.0", - "files": [ - "dist/", - "types/" - ], - "scripts": { - "dev": "concurrently \"npm run build:definitions -- -w\" \"npm run build:bundles -- dev\"", - "build": "nx build", - "build:bundles": "rollup -c", - "build:definitions": "tsc -d --emitDeclarationOnly --declarationDir dist/definitions", - "clean": "rimraf -rf dist/*" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - }, - "devDependencies": { - "@rollup/plugin-typescript": "11.1.6", - "@rollup/pluginutils": "5.1.0", - "acorn": "8.12.1", - "magic-string": "0.30.11", - "rollup": "4.0.0-24", - "typescript": "4.8.3" - } -} diff --git a/packages/rollup-plugin-replace-with-ast/project.json b/packages/rollup-plugin-replace-with-ast/project.json deleted file mode 100644 index ea5f7373d7a..00000000000 --- a/packages/rollup-plugin-replace-with-ast/project.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "rollup-plugin-replace-with-ast", - "private": true, - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "targets": { - "cached:build": { - "executor": "nx:run-commands", - "options": { - "commands": ["npm run build:bundles", "npm run build:definitions"], - "parallel": true, - "cwd": "packages/rollup-plugin-replace-with-ast" - } - }, - "build": { - "dependsOn": ["cached:build"], - "executor": "nx:noop" - } - } -} diff --git a/packages/rollup-plugin-replace-with-ast/rollup.config.mjs b/packages/rollup-plugin-replace-with-ast/rollup.config.mjs deleted file mode 100644 index eb7b91fe275..00000000000 --- a/packages/rollup-plugin-replace-with-ast/rollup.config.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; - -export default { - input: 'src/index.ts', - - output: [ - { - file: 'dist/cjs/index.js', - format: 'cjs', - exports: 'named', - footer: 'module.exports = Object.assign(exports.default, exports);', - sourcemap: true, - }, - { - file: './dist/es/index.js', - format: 'es', - sourcemap: true, - plugins: [emitModulePackageFile()], - }, - ], - plugins: [typescript({sourceMap: true})], -}; - -export function emitModulePackageFile() { - return { - name: 'emit-module-package-file', - generateBundle() { - this.emitFile({ - type: 'asset', - fileName: 'package.json', - source: `{"type":"module"}`, - }); - }, - }; -} diff --git a/packages/rollup-plugin-replace-with-ast/src/index.ts b/packages/rollup-plugin-replace-with-ast/src/index.ts deleted file mode 100644 index 174add14412..00000000000 --- a/packages/rollup-plugin-replace-with-ast/src/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {createFilter} from '@rollup/pluginutils'; -import { - parse, - Node, - ImportDeclaration, - ExportNamedDeclaration, - ExportAllDeclaration, - Program, -} from 'acorn'; -import MagicString from 'magic-string'; - -interface PluginOptions { - include?: string | string[]; - exclude?: string | string[]; - replacements?: Record; -} - -function replaceWithASTPlugin(options: PluginOptions = {}) { - const filter = createFilter(options.include, options.exclude); - const replacements = options.replacements || {}; - - return { - name: 'replace-with-ast-plugin', - - transform(code: string, id: unknown) { - if (!filter(id)) { - return null; - } - - let ast: Program; - try { - ast = parse(code, {ecmaVersion: 2020, sourceType: 'module'}); - } catch (error) { - console.error(`Error parsing ${id}: ${(error as Error).message}`); - return null; - } - - const magicString = new MagicString(code); - - ast.body.forEach((node: Node) => { - if ( - node.type === 'ImportDeclaration' || - node.type === 'ImportDefaultSpecifier' || - node.type === 'ImportSpecifier' || - node.type === 'ImportNamespaceSpecifier' || - node.type === 'ExportSpecifier' || - node.type === 'ExportDefaultSpecifier' || - node.type === 'ExportNamedDeclaration' || - node.type === 'ExportAllDeclaration' - ) { - const source = ( - node as - | ImportDeclaration - | ExportNamedDeclaration - | ExportAllDeclaration - ).source; - if ( - source && - typeof source.value === 'string' && - replacements[source.value] - ) { - const start = source.start; - const end = source.end; - magicString.overwrite( - start, - end, - JSON.stringify(replacements[source.value]) - ); - } - } - }); - - return { - code: magicString.toString(), - map: magicString.generateMap({hires: true}), - }; - }, - }; -} - -export default replaceWithASTPlugin; diff --git a/packages/rollup-plugin-replace-with-ast/tsconfig.json b/packages/rollup-plugin-replace-with-ast/tsconfig.json deleted file mode 100644 index 1c7b2d4da00..00000000000 --- a/packages/rollup-plugin-replace-with-ast/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "lib": ["es6"], - "module": "esnext", - "moduleResolution": "node", - "skipLibCheck": true, - "noEmitOnError": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "pretty": true, - "sourceMap": true, - "strict": true, - "target": "es2019" - }, - "exclude": ["./dist/**", "./test/types/**"] -} diff --git a/packages/rollup-plugin-replace-with-ast/types/index.d.ts b/packages/rollup-plugin-replace-with-ast/types/index.d.ts deleted file mode 100644 index a94814dbf52..00000000000 --- a/packages/rollup-plugin-replace-with-ast/types/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Plugin } from 'rollup'; - -export interface PluginOptions { - include?: string | string[]; - exclude?: string | string[]; - replacements?: Record; -} - -/** - * A Rollup plugin for replacing import/export paths using AST. - */ -export default function replaceWithASTPlugin(options?: PluginOptions): Plugin; diff --git a/packages/samples/angular/package.json b/packages/samples/angular/package.json index 6eeee45f6e6..8999f4b2fc7 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.2.0", "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..45b07f57185 100644 --- a/packages/samples/atomic-next/package.json +++ b/packages/samples/atomic-next/package.json @@ -4,9 +4,9 @@ "private": true, "type": "module", "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/atomic-react": "3.2.0", + "@coveo/headless": "3.5.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/packages/samples/atomic-react/package.json b/packages/samples/atomic-react/package.json index 12edd1a08ff..8647b1b065d 100644 --- a/packages/samples/atomic-react/package.json +++ b/packages/samples/atomic-react/package.json @@ -4,9 +4,9 @@ "description": "Samples with atomic-react", "private": true, "dependencies": { - "@coveo/atomic": "3.4.0", - "@coveo/atomic-react": "3.1.6", - "@coveo/headless": "3.4.0", + "@coveo/atomic": "3.7.0", + "@coveo/atomic-react": "3.2.0", + "@coveo/headless": "3.5.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/packages/samples/headless-commerce-react/package.json b/packages/samples/headless-commerce-react/package.json index f663b12e7e5..c862c93458c 100644 --- a/packages/samples/headless-commerce-react/package.json +++ b/packages/samples/headless-commerce-react/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "dependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx b/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx index 72cc88acbaf..5826c94404e 100644 --- a/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx +++ b/packages/samples/headless-commerce-react/src/components/breadcrumb-manager/breadcrumb-manager.tsx @@ -4,6 +4,7 @@ import { NumericFacetValue, DateFacetValue, BreadcrumbManager as HeadlessBreadcrumbManager, + LocationFacetValue, } from '@coveo/headless/commerce'; import {useEffect, useState} from 'react'; @@ -29,7 +30,8 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) { | CategoryFacetValue | RegularFacetValue | NumericFacetValue - | DateFacetValue, + | DateFacetValue + | LocationFacetValue, type: string ) => { switch (type) { @@ -50,6 +52,7 @@ export default function BreadcrumbManager(props: BreadcrumbManagerProps) { (value as DateFacetValue).end ); default: + // TODO COMHUB-292 add location facet example return null; } }; diff --git a/packages/samples/headless-react/package.json b/packages/samples/headless-react/package.json index cd47471f5f7..f7938ea35ea 100644 --- a/packages/samples/headless-react/package.json +++ b/packages/samples/headless-react/package.json @@ -5,11 +5,11 @@ "private": true, "type": "module", "engines": { - "node": "^20.9.0" + "node": "^20.9.0 || ^22.11.0" }, "dependencies": { "@coveo/auth": "2.0.1", - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", @@ -21,7 +21,7 @@ "@types/react-router-dom": "5.3.3", "dayjs": "1.11.12", "escape-html": "1.0.3", - "express": "4.19.2", + "express": "4.20.0", "filesize": "10.1.4", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx new file mode 100644 index 00000000000..6e4eef604ae --- /dev/null +++ b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx @@ -0,0 +1,84 @@ +import { + BreadcrumbManagerState, + NumericFacetValue, + DateFacetValue, + CategoryFacetValue, + BreadcrumbManager as HeadlessBreadcrumbManager, + RegularFacetValue, + LocationFacetValue, +} from '@coveo/headless/ssr-commerce'; +import {useEffect, useState} from 'react'; + +interface BreadcrumbManagerProps { + staticState: BreadcrumbManagerState; + controller?: HeadlessBreadcrumbManager; +} + +export default function BreadcrumbManager(props: BreadcrumbManagerProps) { + const {staticState, controller} = props; + + const [state, setState] = useState(staticState); + + useEffect(() => { + controller?.subscribe(() => setState(controller.state)); + }, [controller]); + + const renderBreadcrumbValue = ( + value: + | CategoryFacetValue + | RegularFacetValue + | NumericFacetValue + | DateFacetValue + | LocationFacetValue, + type: string + ) => { + switch (type) { + case 'hierarchical': + return (value as CategoryFacetValue).path.join(' > '); + case 'regular': + return (value as RegularFacetValue).value; + case 'numericalRange': + return ( + (value as NumericFacetValue).start + + ' - ' + + (value as NumericFacetValue).end + ); + case 'dateRange': + return ( + (value as DateFacetValue).start + + ' - ' + + (value as DateFacetValue).end + ); + default: + // TODO COMHUB-291 support location breadcrumb + return null; + } + }; + + return ( +
    +
    + +
    +
      + {state.facetBreadcrumbs.map((facetBreadcrumb) => { + return ( +
    • + {facetBreadcrumb.values.map((value, index) => { + return ( + + ); + })} +
    • + ); + })} +
    +
    + ); +} diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx index f4e554f08be..05612968ed2 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx @@ -7,6 +7,7 @@ import { ListingHydratedState, ListingStaticState, } from '../../_lib/commerce-engine'; +import BreadcrumbManager from '../breadcrumb-manager'; import Cart from '../cart'; import FacetGenerator from '../facets/facet-generator'; import Pagination from '../pagination'; @@ -63,6 +64,10 @@ export default function ListingPage({ hydratedState?.controllers.instantProducts } /> + + coveo/renovate-presets", + "helpers:pinGitHubActionDigests" ], "labels": ["dependencies"], "packageRules": [ @@ -12,6 +11,13 @@ "groupName": "all dependencies", "groupSlug": "all" }, + { + "matchPackagePatterns": ["typedoc"], + "groupName": "typedoc", + "groupSlug": "typedoc", + "rangeStrategy": "replace", + "description": "Isolate typedoc updates to monitor for breaking changes" + }, { "matchPackagePatterns": [ "^@angular/*", @@ -155,6 +161,5 @@ "commitMessageSuffix": "J:KIT-282", "vulnerabilityAlerts": { "enabled": false - }, - "ignoreDeps": ["@monaco-editor/react"] + } } diff --git a/scripts/ci/determine-shard.mjs b/scripts/ci/determine-shard.mjs deleted file mode 100644 index 402ef56775f..00000000000 --- a/scripts/ci/determine-shard.mjs +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env node -import {setOutput} from '@actions/core'; - -function getOutputName() { - return process.argv.slice(2, 4); -} - -function allocateShards(testCount, maximumShards) { - const shardTotal = - testCount === 0 ? maximumShards : Math.min(testCount, maximumShards); - const shardIndex = Array.from({length: shardTotal}, (_, i) => i + 1); - return [shardIndex, [shardTotal]]; -} - -const testsToRun = process.env.testsToRun.split(' '); -const maximumShards = parseInt(process.env.maximumShards, 10); - -const [shardIndexOutputName, shardTotalOutputName] = getOutputName(); -const [shardIndex, shardTotal] = allocateShards( - testsToRun.length, - maximumShards -); - -setOutput(shardIndexOutputName, shardIndex); -setOutput(shardTotalOutputName, shardTotal); diff --git a/scripts/ci/determine-tests.mjs b/scripts/ci/determine-tests.mjs new file mode 100644 index 00000000000..74d7d9fd665 --- /dev/null +++ b/scripts/ci/determine-tests.mjs @@ -0,0 +1,221 @@ +#!/usr/bin/env node +import {setOutput} from '@actions/core'; +import {readdirSync, statSync} from 'fs'; +import {EOL} from 'os'; +import {basename, dirname, join, relative} from 'path'; +import {getBaseHeadSHAs, getChangedFiles} from './hasFileChanged.mjs'; +import {listImports, ensureFileExists} from './list-imports.mjs'; + +class NoRelevantChangesError extends Error { + constructor() { + super('No changes that would affect Atomic were detected. Skipping tests.'); + this.name = 'NoRelevantChangesError'; + } +} + +class DependentPackageChangeError extends Error { + constructor(file) { + super( + `Changes detected in a package on which Atomic depend: ${file}. Running all tests.` + ); + this.name = 'DependentPackageChangeError'; + this.file = file; + } +} + +class PackageJsonChangeError extends Error { + constructor(file) { + super( + `Changes detected in a package.json or package-lock.json file: ${file}. Running all tests.` + ); + this.name = 'PackageJsonChangeError'; + this.file = file; + } +} + +/** + * Recursively finds all end-to-end test files with the `.e2e.ts` extension in a given directory. + * + * @param {string} dir - The directory to search for test files. + * @returns {string[]} An array of paths to the found test files. + */ +function findAllTestFiles(dir) { + function searchFiles(currentDir, testFiles) { + const files = readdirSync(currentDir); + + for (const file of files) { + const fullPath = join(currentDir, file); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + searchFiles(fullPath, testFiles); + } else if (fullPath.endsWith('.e2e.ts')) { + testFiles.push(fullPath); + } + } + + return testFiles; + } + + return searchFiles(dir, []); +} + +/** + * Creates a mapping of test file names to their respective import dependencies. + * + * @param {string[]} testPaths - An array of paths to the test files. + * @param {string} projectRoot - The root directory of the project. + * @returns {Map>} A map where the key is the test file name and the value is a set of import dependencies. + */ +function createTestFileMappings(testPaths, projectRoot) { + const testFileMappings = testPaths.map((testPath) => { + const imports = new Set(); + const testName = basename(testPath); + const sourceFilePath = join( + dirname(testPath).replace('/e2e', ''), + testName.replace('.e2e.ts', '.tsx') + ); + + ensureFileExists(sourceFilePath); + + [ + relative(projectRoot, sourceFilePath), + ...listImports(projectRoot, sourceFilePath), + ...listImports(projectRoot, testPath), + ].forEach((importedFile) => imports.add(importedFile)); + + return [testName, imports]; + }); + + return new Map(testFileMappings); +} + +/** + * Determines which test files need to be run based on the changed files and their dependencies. + * + * @param {string[]} changedFiles - An array of file paths that have been changed. + * @param {Map>} testDependencies - A map where the keys are test file paths and the values are sets of source file paths that the test files depend on. + * @returns {string} A space-separated string of test file paths that need to be run. + */ +function determineTestFilesToRun(changedFiles, testDependencies) { + const testsToRun = new Set(); + for (const changedFile of changedFiles) { + for (const [testFile, sourceFiles] of testDependencies) { + ensureIsNotCoveoPackage(changedFile); + ensureIsNotPackageJsonOrPackageLockJson(changedFile); + const isChangedTestFile = testFile === basename(changedFile); + const isAffectedSourceFile = sourceFiles.has(changedFile); + if (isChangedTestFile || isAffectedSourceFile) { + testsToRun.add(testFile); + testDependencies.delete(testFile); + } + } + } + return [...testsToRun].join(' '); +} + +/** + * Ensures that the given file is not part of a Coveo package. + * Throws an error if the file depends on a Coveo package. + * + * @param {string} file - The path to the file to check. + * @throws {DependentPackageChangeError} If the file depends on a Coveo package. + */ +function ensureIsNotCoveoPackage(file) { + if (dependsOnCoveoPackage(file)) { + throw new DependentPackageChangeError(file); + } +} + +/** + * Ensures that the provided file is not 'package.json' or 'package-lock.json'. + * Throws a PackageJsonChangeError if the file is either of these. + * + * @param {string} file - The name or path of the file to check. + * @throws {PackageJsonChangeError} If the file is 'package.json' or 'package-lock.json'. + */ +function ensureIsNotPackageJsonOrPackageLockJson(file) { + if (file.includes('package.json') || file.includes('package-lock.json')) { + throw new PackageJsonChangeError(file); + } +} + +/** + * Checks if a given file depends on any of the specified external Coveo packages. + * + * @param {string} file - The path of the file to check. + * @returns {boolean} - Returns true if the file path includes any of the external package paths, otherwise false. + */ +function dependsOnCoveoPackage(file) { + const externalPackages = ['packages/headless/', 'packages/bueno/']; + for (const pkg of externalPackages) { + if (file.includes(pkg)) { + return true; + } + } +} + +/** + * Allocates test shards based on the total number of tests and the maximum number of shards. + * + * @param {number} testCount - The total number of tests. + * @param {number} maximumShards - The maximum number of shards to allocate. + * @returns {[number[], number[]]} An array containing two elements: + * - The first element is an array of shard indices. + * - The second element is an array containing the total number of shards. + */ +function allocateShards(testCount, maximumShards) { + const shardTotal = + testCount === 0 ? maximumShards : Math.min(testCount, maximumShards); + const shardIndex = Array.from({length: shardTotal}, (_, i) => i + 1); + return [shardIndex, [shardTotal]]; +} + +const {base, head} = getBaseHeadSHAs(); +const changedFiles = getChangedFiles(base, head).split(EOL); +const outputNameTestsToRun = process.argv[2]; +const outputNameShardIndex = process.argv[3]; +const outputNameShardTotal = process.argv[4]; +const projectRoot = process.env.projectRoot; +const atomicSourceComponents = join('packages', 'atomic', 'src', 'components'); + +try { + const testFiles = findAllTestFiles(atomicSourceComponents); + const testDependencies = createTestFileMappings(testFiles, projectRoot); + const testsToRun = determineTestFilesToRun(changedFiles, testDependencies); + if (testsToRun === '') { + throw new NoRelevantChangesError(); + } + const maximumShards = parseInt(process.env.maximumShards, 10); + + const [shardIndex, shardTotal] = allocateShards( + testsToRun.split(' ').length, + maximumShards + ); + + setOutput(outputNameTestsToRun, testsToRun); + setOutput(outputNameShardIndex, shardIndex); + setOutput(outputNameShardTotal, shardTotal); +} catch (error) { + if (error instanceof NoRelevantChangesError) { + console.warn(error?.message || error); + setOutput(outputNameTestsToRun, ''); + setOutput(outputNameShardIndex, [0]); + setOutput(outputNameShardTotal, [0]); + } + + if ( + error instanceof DependentPackageChangeError || + error instanceof PackageJsonChangeError + ) { + console.warn(error?.message || error); + setOutput(outputNameTestsToRun, ''); + const shardIndex = Array.from( + {length: process.env.maximumShards}, + (_, i) => i + 1 + ); + setOutput(outputNameShardIndex, shardIndex); + const shardTotal = [process.env.maximumShards]; + setOutput(outputNameShardTotal, shardTotal); + } +} diff --git a/scripts/ci/find-tests.mjs b/scripts/ci/find-tests.mjs deleted file mode 100644 index a8d0b11bdcb..00000000000 --- a/scripts/ci/find-tests.mjs +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env node -import {setOutput} from '@actions/core'; -import {readdirSync, statSync} from 'fs'; -import {EOL} from 'os'; -import {basename, dirname, join, relative} from 'path'; -import { - getBaseHeadSHAs, - getChangedFiles, - getOutputName, -} from './hasFileChanged.mjs'; -import {listImports, ensureFileExists} from './list-imports.mjs'; - -/** - * Recursively searches for all end-to-end (E2E) test files in a given directory. - * E2E test files are identified by the `.e2e.ts` file extension. - * - * @param dir - The root directory to start the search from. - * @returns An array of strings, each representing the full path to an E2E test file. - */ -function findAllTestFiles(dir) { - function searchFiles(currentDir, testFiles) { - const files = readdirSync(currentDir); - - for (const file of files) { - const fullPath = join(currentDir, file); - const stat = statSync(fullPath); - - if (stat.isDirectory()) { - searchFiles(fullPath, testFiles); - } else if (fullPath.endsWith('.e2e.ts')) { - testFiles.push(fullPath); - } - } - - return testFiles; - } - - return searchFiles(dir, []); -} - -/** - * Creates a mapping of test file names to the set of files they import. - * - * @param testPaths - An array of E2E test file paths. - * @returns A map where each key is a test file name and the value is the set of files it imports. - */ -function createTestFileMappings(testPaths, projectRoot) { - const testFileMappings = testPaths.map((testPath) => { - const imports = new Set(); - const testName = basename(testPath); - const sourceFilePath = join( - dirname(testPath).replace('/e2e', ''), - testName.replace('.e2e.ts', '.tsx') - ); - - ensureFileExists(sourceFilePath); - - [ - relative(projectRoot, sourceFilePath), - ...listImports(projectRoot, sourceFilePath), - ...listImports(projectRoot, testPath), - ].forEach((importedFile) => imports.add(importedFile)); - - return [testName, imports]; - }); - - return new Map(testFileMappings); -} - -/** - * Determines which E2E test files to run based on the files that have changed. - * - * @param changedFiles - An array of files that have changed. - * @param testDependencies - A map of test file names to the set of files they import. - * @returns A space-separated string of test files to run. - */ -function determineTestFilesToRun(changedFiles, testDependencies) { - const testsToRun = new Set(); - for (const changedFile of changedFiles) { - for (const [testFile, sourceFiles] of testDependencies) { - ensureIsNotCoveoPackage(changedFile); - const isChangedTestFile = testFile === basename(changedFile); - const isAffectedSourceFile = sourceFiles.has(changedFile); - if (isChangedTestFile || isAffectedSourceFile) { - testsToRun.add(testFile); - testDependencies.delete(testFile); - } - } - } - return [...testsToRun].join(' '); -} - -function ensureIsNotCoveoPackage(file) { - if (dependsOnCoveoPackage(file)) { - throw new Error('Change detected in an different Coveo package.'); - } -} - -function dependsOnCoveoPackage(file) { - const externalPackages = ['packages/headless', 'packages/bueno']; - for (const pkg of externalPackages) { - if (file.includes(pkg)) { - return true; - } - } -} - -const {base, head} = getBaseHeadSHAs(); -const changedFiles = getChangedFiles(base, head).split(EOL); -const outputName = getOutputName(); -const projectRoot = process.env.projectRoot; -const atomicSourceComponents = join('packages', 'atomic', 'src', 'components'); - -try { - const testFiles = findAllTestFiles(atomicSourceComponents); - const testDependencies = createTestFileMappings(testFiles, projectRoot); - const testsToRun = determineTestFilesToRun(changedFiles, testDependencies); - setOutput(outputName, testsToRun ? testsToRun : '--grep @no-test'); - - if (!testsToRun) { - console.log('No relevant source file changes detected for E2E tests.'); - } -} catch (error) { - console.warn(error?.message || error); -} diff --git a/scripts/ci/hasFileChanged.mjs b/scripts/ci/hasFileChanged.mjs index a268f258209..26624ab1b43 100644 --- a/scripts/ci/hasFileChanged.mjs +++ b/scripts/ci/hasFileChanged.mjs @@ -41,7 +41,7 @@ function checkPatterns(files, patterns) { return false; } -export function getOutputName() { +function getOutputName() { return process.argv[2]; } diff --git a/scripts/ci/package-compatibility.mjs b/scripts/ci/package-compatibility.mjs new file mode 100644 index 00000000000..3870bbdf467 --- /dev/null +++ b/scripts/ci/package-compatibility.mjs @@ -0,0 +1,73 @@ +import {EOL} from 'os'; +import {publint} from 'publint'; + +let exitCode = 0; + +const pkgDirs = [ + 'packages/atomic', + 'packages/headless', + 'packages/atomic-react', +]; + +const issues = await Promise.all( + pkgDirs.map(async (pkgDir) => { + const {messages} = await publint({ + pkgDir, + level: 'suggestion', + strict: false, + }); + + const suggestions = []; + const warnings = []; + const errors = []; + + for (const message of messages) { + switch (message.type) { + case 'suggestion': + suggestions.push(message); + break; + case 'warning': + warnings.push(message); + break; + case 'error': + errors.push(message); + break; + } + } + + return { + pkgDir, + suggestions, + warnings, + errors, + }; + }) +); + +function prettyPrintJSON(label, jsonObject, logFunction) { + logFunction(`${label}:`, EOL); + logFunction(JSON.stringify(jsonObject, null, 2), EOL); +} + +if (issues.length > 0) { + console.info('The publint scan detected compatibility issues:\n'); + + for (const {errors, warnings, suggestions, pkgDir} of issues) { + console.group(`\n********** ${pkgDir} **********\n`); + if (errors.length > 0) { + exitCode = 1; + prettyPrintJSON('Errors', errors, console.error); + } + if (warnings.length > 0) { + prettyPrintJSON('Warnings', warnings, console.warn); + } + if (suggestions.length > 0) { + prettyPrintJSON('Suggestions', suggestions, console.info); + } + console.groupEnd(); + } +} else { + console.info('No compatibility issues found by publint.'); +} + +process.exit(exitCode); 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); diff --git a/vitest.workspace.js b/vitest.workspace.js new file mode 100644 index 00000000000..ff4c73d9a1a --- /dev/null +++ b/vitest.workspace.js @@ -0,0 +1,10 @@ +import {defineWorkspace} from 'vitest/config'; + +export default defineWorkspace([ + './packages/headless/vitest.config.js', + './packages/samples/headless-commerce-react/vite.config.js', + './packages/samples/headless-react/vite.config.js', + // eslint-disable-next-line @cspell/spellchecker + './packages/samples/vuejs/vite.config.ts', + './packages/headless-react/vitest.config.js', +]);