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/workflows/prbot.yml b/.github/workflows/prbot.yml index 68b0a948148..e44d6c36c34 100644 --- a/.github/workflows/prbot.yml +++ b/.github/workflows/prbot.yml @@ -94,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: @@ -104,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 @@ -123,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/package-lock.json b/package-lock.json index 87ad04e4579..ae7dc3bdd6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11461,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", @@ -20613,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", @@ -24280,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", @@ -24402,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", @@ -27909,6 +28017,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", @@ -33676,6 +33798,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", @@ -33698,6 +33894,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", @@ -34004,6 +34224,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", @@ -40264,6 +40495,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", @@ -40737,6 +40975,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", @@ -40822,6 +41092,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", @@ -43448,6 +43812,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", @@ -46597,6 +46974,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", @@ -48472,6 +48856,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", @@ -49604,6 +50013,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", @@ -50597,6 +51032,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", @@ -51372,6 +51818,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", @@ -51756,6 +52244,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", @@ -53539,13 +54071,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.6.2", + "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", @@ -53642,8 +54185,8 @@ "node": "^20.9.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": { @@ -53658,14 +54201,14 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic": "3.6.2", + "@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", @@ -53681,7 +54224,7 @@ "node": "^20.9.0" }, "peerDependencies": { - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" } }, "packages/atomic-angular/node_modules/jasmine-core": { @@ -53710,10 +54253,10 @@ }, "packages/atomic-angular/projects/atomic-angular": { "name": "@coveo/atomic-angular", - "version": "3.1.10", + "version": "3.2.0", "license": "Apache-2.0", "dependencies": { - "@coveo/atomic": "3.6.2", + "@coveo/atomic": "3.7.0", "tslib": "2.6.3" }, "engines": { @@ -53722,16 +54265,16 @@ "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": { @@ -53813,9 +54356,9 @@ }, "packages/atomic-react": { "name": "@coveo/atomic-react", - "version": "3.1.10", + "version": "3.2.0", "dependencies": { - "@coveo/atomic": "3.6.2" + "@coveo/atomic": "3.7.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -53837,7 +54380,7 @@ "node": "^20.9.0" }, "peerDependencies": { - "@coveo/headless": "3.4.0", + "@coveo/headless": "3.5.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" } @@ -57158,7 +57701,7 @@ }, "packages/bueno": { "name": "@coveo/bueno", - "version": "1.0.1", + "version": "1.0.2", "license": "Apache-2.0", "devDependencies": { "@coveo/release": "1.0.0", @@ -57219,10 +57762,10 @@ }, "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", @@ -57248,6 +57791,7 @@ "execa": "8.0.1", "install": "0.13.0", "ts-node": "10.9.2", + "typedoc": "0.26.10", "vitest": "2.1.1" }, "engines": { @@ -57260,10 +57804,10 @@ }, "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", @@ -57945,6 +58489,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", @@ -57973,6 +58530,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", @@ -58003,6 +58570,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", @@ -58079,6 +58678,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", @@ -58092,6 +58730,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", @@ -58208,14 +58853,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.5", + "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" }, @@ -59931,7 +60589,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@coveo/atomic-angular": "3.1.10", + "@coveo/atomic-angular": "3.2.0", "rxjs": "7.8.1", "tslib": "2.6.3", "zone.js": "0.14.8" @@ -60231,9 +60889,9 @@ "name": "@coveo/atomic-next-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.6.2", - "@coveo/atomic-react": "3.1.10", - "@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" @@ -60296,9 +60954,9 @@ "name": "@coveo/atomic-react-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.6.2", - "@coveo/atomic-react": "3.1.10", - "@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" }, @@ -60786,7 +61444,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" }, @@ -62079,7 +62737,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", @@ -64462,8 +65120,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" @@ -64484,7 +65142,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" @@ -64603,10 +65261,10 @@ "version": "0.1.0", "dependencies": { "@babel/standalone": "7.25.0", - "@coveo/atomic": "3.6.2", - "@coveo/atomic-hosted-page": "1.0.7", - "@coveo/atomic-react": "3.1.10", - "@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" }, @@ -64675,9 +65333,9 @@ "name": "@coveo/atomic-stencil-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.6.2", - "@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" }, @@ -64960,7 +65618,7 @@ "name": "@coveo/atomic-vuejs-samples", "version": "0.0.0", "dependencies": { - "@coveo/atomic": "3.6.2", + "@coveo/atomic": "3.7.0", "vue": "^3.4.15" }, "devDependencies": { diff --git a/packages/atomic-angular/package.json b/packages/atomic-angular/package.json index 7eed33f803a..73732a19a02 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.6.2", + "@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", 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 762be0c6edd..2b5cfb9f6ba 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.10", + "version": "3.2.0", "license": "Apache-2.0", "repository": { "url": "https://github.com/coveo/ui-kit" @@ -8,10 +8,10 @@ "peerDependencies": { "@angular/common": "14 - 17", "@angular/core": "14 - 17", - "@coveo/headless": "3.4.0" + "@coveo/headless": "3.5.0" }, "dependencies": { - "@coveo/atomic": "3.6.2", + "@coveo/atomic": "3.7.0", "tslib": "2.6.3" }, "engines": { 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..200e7f34a41 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,6 +62,7 @@ AtomicPopover, AtomicProduct, AtomicProductChildren, AtomicProductDescription, +AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, @@ -205,6 +206,7 @@ AtomicPopover, AtomicProduct, AtomicProductChildren, AtomicProductDescription, +AtomicProductExcerpt, AtomicProductFieldCondition, AtomicProductImage, AtomicProductLink, 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 5fa2cee9ec5..c83226b2dee 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 @@ -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; @@ -1299,6 +1299,28 @@ export class AtomicProductDescription { export declare interface AtomicProductDescription extends Components.AtomicProductDescription {} +@ProxyCmp({ + 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'] }) 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 080edc90fd4..66b70fa1a37 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", @@ -30,8 +30,8 @@ "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": { diff --git a/packages/atomic-react/CHANGELOG.md b/packages/atomic-react/CHANGELOG.md index edca716873a..322b54ee588 100644 --- a/packages/atomic-react/CHANGELOG.md +++ b/packages/atomic-react/CHANGELOG.md @@ -1,3 +1,12 @@ +## 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) diff --git a/packages/atomic-react/package.json b/packages/atomic-react/package.json index 288880e9d22..c114ad339ca 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.10", + "version": "3.2.0", "description": "React specific wrapper for the Atomic component library", "repository": { "type": "git", @@ -30,7 +30,7 @@ "commerce/" ], "dependencies": { - "@coveo/atomic": "3.6.2" + "@coveo/atomic": "3.7.0" }, "devDependencies": { "@coveo/release": "1.0.0", @@ -49,7 +49,7 @@ "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" }, 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..ed83c86f8f1 100644 --- a/packages/atomic-react/src/components/stencil-generated/commerce/index.ts +++ b/packages/atomic-react/src/components/stencil-generated/commerce/index.ts @@ -41,6 +41,7 @@ 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'); diff --git a/packages/atomic/CHANGELOG.md b/packages/atomic/CHANGELOG.md index 601c16fc587..2c1e7e7d07d 100644 --- a/packages/atomic/CHANGELOG.md +++ b/packages/atomic/CHANGELOG.md @@ -1,3 +1,16 @@ +## 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) diff --git a/packages/atomic/package.json b/packages/atomic/package.json index ec705099797..a056f242fa3 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -1,7 +1,7 @@ { "name": "@coveo/atomic", "type": "module", - "version": "3.6.2", + "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": { @@ -65,8 +65,8 @@ "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", @@ -160,8 +160,8 @@ "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": { diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index a2ac7066608..beb42d4e7ae 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"; @@ -2072,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. @@ -4909,6 +4928,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`). @@ -6061,6 +6089,7 @@ 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; @@ -8080,10 +8109,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. @@ -9801,6 +9847,7 @@ 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; @@ -10251,6 +10298,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`). 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-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-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-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/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-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/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/pages/examples/commerce-website/search.html b/packages/atomic/src/pages/examples/commerce-website/search.html index 3420e6fd93d..010027eebef 100644 --- a/packages/atomic/src/pages/examples/commerce-website/search.html +++ b/packages/atomic/src/pages/examples/commerce-website/search.html @@ -77,7 +77,20 @@

Search page

diff --git a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html index 058def95d12..0e0f37c4601 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html +++ b/packages/quantic/force-app/main/default/lwc/quanticRefineModalContent/templates/dynamicNavigation.html @@ -121,6 +121,7 @@ no-filter-facet-count={facet.noFilterFacetCount} injection-depth={facet.injectionDepth} key={facet.field} + custom-sort={facet.customSort} is-collapsed > 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 6ec19618e4f..bda5160552f 100644 --- a/packages/quantic/package.json +++ b/packages/quantic/package.json @@ -1,6 +1,6 @@ { "name": "@coveo/quantic", - "version": "3.2.5", + "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", @@ -45,8 +45,8 @@ "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" }, diff --git a/packages/samples/angular/package.json b/packages/samples/angular/package.json index 0fc577ccf5f..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.10", + "@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 a423584f792..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.6.2", - "@coveo/atomic-react": "3.1.10", - "@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 14fb8d33c3a..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.6.2", - "@coveo/atomic-react": "3.1.10", - "@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 12edb0a74fe..f1893c1031f 100644 --- a/packages/samples/headless-react/package.json +++ b/packages/samples/headless-react/package.json @@ -9,7 +9,7 @@ }, "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", 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 } /> + + 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 ccf609fc719..00000000000 --- a/scripts/ci/find-tests.mjs +++ /dev/null @@ -1,126 +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: ${file}`); - } -} - -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); - setOutput(outputName, ''); // Passing an empty string will run all tests. -} 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/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', +]);