diff --git a/.eslintrc.js b/.eslintrc.js index 7b5ebef5325..8121a04e34b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,7 @@ module.exports = { 'prop-types', 'react', 'requireindex', + 'react-transition-group', ], 'import/resolver': { node: {}, diff --git a/.github/monitor-typescript-errors/index.js b/.github/monitor-typescript-errors/index.js index d08055827a3..29544181123 100644 --- a/.github/monitor-typescript-errors/index.js +++ b/.github/monitor-typescript-errors/index.js @@ -1,9 +1,8 @@ const fs = require( 'fs' ); const { getOctokit, context } = require( '@actions/github' ); -const { setFailed, getInput, setOutput } = require( '@actions/core' ); +const { getInput, setOutput } = require( '@actions/core' ); const { parseXml, getFilesWithNewErrors } = require( './utils/xml' ); const { generateMarkdownMessage } = require( './utils/markdown' ); -const { addRecord } = require( './utils/airtable' ); const { addComment } = require( './utils/github' ); const runner = async () => { @@ -58,13 +57,17 @@ const runner = async () => { } } - if ( process.env[ 'CURRENT_BRANCH' ] === 'trunk' ) { - try { - await addRecord( currentCheckStyleFileContentParsed.totalErrors ); - } catch ( error ) { - setFailed( error ); - } - } + /** + * @todo: Airtable integration is failing auth, so we're disabling it for now. + * Issue opened: https://github.com/woocommerce/woocommerce-blocks/issues/8961 + */ + // if ( process.env[ 'CURRENT_BRANCH' ] === 'trunk' ) { + // try { + // await addRecord( currentCheckStyleFileContentParsed.totalErrors ); + // } catch ( error ) { + // setFailed( error ); + // } + // } }; runner(); diff --git a/.github/patch-initial-checklist.md b/.github/patch-initial-checklist.md index 6b031de72cd..09f26c81ec9 100644 --- a/.github/patch-initial-checklist.md +++ b/.github/patch-initial-checklist.md @@ -13,6 +13,7 @@ The release pull request has been created! This checklist is a guide to follow f - [ ] Check the changelog matches the one in the pull request description above. - [ ] Run `npm run change-versions` to update the version numbers in several files. Write the version number you are releasing: {{version}}. - [ ] Update compatibility sections (if applicable). +- [ ] Cherry-pick into the release branch all fixes that need to be included in this release (assuming they were merged into `trunk`). - [ ] Push above changes to the release branch. ## Create the Testing Notes @@ -62,10 +63,7 @@ Each porter is responsible for testing the PRs that fall under the focus of thei ## After Deploy -- [ ] Merge this branch back into the base branch. - - If the base branch was `trunk`, and this release was deployed to WordPress.org, then merge the branch into `trunk`. - - If the base branch is the release branch this patch release is for, then merge branch into release branch and then merge the release branch back to `trunk` if the patch release is the latest released version. Otherwise just merge back into the release branch. -- [ ] If you merged the branch to `trunk`, then update version on the `trunk` branch to be for the next version of the plugin and include the `dev` suffix (e.g. something like [`2.6-dev`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/commit/e27f053e7be0bf7c1d376f5bdb9d9999190ce158)) for the next version. +- [ ] Port to `trunk` the changes to the changelog, testing steps and required versions that you did in the previous steps. You can do so copy-and-pasting the changes in a new commit directly to `trunk`, or cherry-picking the commits that introduced those changes. - [ ] Update the schedules p2 with the shipped date for the release (PdToLP-K-p2). - [ ] Edit the GitHub milestone of the release you just shipped and add the current date as the due date (this is used to track ship date as well). @@ -92,8 +90,11 @@ This only needs done if the patch release needs to be included in WooCommerce Co - It lists all the WooCommerce Blocks versions that are being included since the last version that you edited in `plugins/woocommerce/composer.json`. Each version should have a link for the `Release PR`, `Testing instructions` and `Release post` (if available). - The changelog should be aggregated from all the releases included in the package bump and grouped per type: `Enhancements`, `Bug Fixes`, `Various` etc. This changelog will be used in the release notes for the WooCommerce release. That's why it should only list the PRs that have WooCoomerce Core in the WooCommerce Visibility section of their description. Don't include changes available in the feature plugin or development builds. -- Run through the testing checklist to ensure everything works in that branch for that package bump. **Note:** Testing should ensure any features/new blocks that are supposed to be behind feature gating for the core merge of this package update are working as expected. -- Testing should include completing the [Smoke testing checklist](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/internal-developers/testing/smoke-testing.md). It's up to you to verify that those tests have been done. +- [ ] Build WC core from that branch with `pnpm run --filter='woocommerce' build ` (you might need to [install the dependencies first](https://github.com/woocommerce/woocommerce#prerequisites)) and: + + - [ ] Make sure the correct version of WC Blocks is being loaded. This can be done testing at least one of the testing steps from the release. + - [ ] Complete the [Smoke testing checklist](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/internal-developers/testing/smoke-testing.md). + - [ ] Verify and make any additional edits to the pull request description for things like: Changelog to be included with WooCommerce core, additional communication that might be needed elsewhere, additional marketing communication notes that may be needed, etc. - [ ] Assign the corresponding WC version milestone to the PR @@ -111,3 +112,4 @@ You only need to post public release announcements and update relevant public fa - Don't forget to use category `WooCommerce Blocks Release Notes` for the post. - [ ] Announce the release internally (`#woo-announcements` slack). - [ ] Go through the description of the release pull request and edit it to update all the sections and checklist instructions there. +- [ ] Close this PR. diff --git a/.github/release-initial-checklist.md b/.github/release-initial-checklist.md index 772ee97f695..037c1c7d57a 100644 --- a/.github/release-initial-checklist.md +++ b/.github/release-initial-checklist.md @@ -78,7 +78,7 @@ Each porter is responsible for testing the PRs that fall under the focus of thei ## After Workflow completes -- [ ] Merge this pull request back into `trunk`. This may have merge conflicts needing resolved if there are any cherry-picked commits in the release branch. +- [ ] Port to `trunk` the changes to the changelog, testing steps and required versions that you did in the previous steps. You can do so copy-and-pasting the changes in a new commit directly to `trunk`, or cherry-picking the commits that introduced those changes. - [ ] Run `npm run change-versions` to update the version in `trunk` to the next version of the plugin and include the `dev` suffix. For example, if you released 2.5.0, you should update the version in `trunk` to 2.6.0-dev. - [ ] Update the schedules p2 with the shipped date for the release (PdToLP-K-p2). - [ ] Edit the GitHub milestone of the release you just shipped and add the current date as the due date (this is used to track ship date as well). @@ -106,11 +106,16 @@ This only needs to be done if this release is the last release of the feature pl - The PR description can follow [this example](https://github.com/woocommerce/woocommerce/pull/32627). - It lists all the WooCommerce Blocks versions that are being included since the last version that you edited in `plugins/woocommerce/composer.json`. Each version should have a link for the `Release PR`, `Testing instructions` and `Release post` (if available). - The changelog should be aggregated from all the releases included in the package bump and grouped per type: `Enhancements`, `Bug Fixes`, `Various` etc. This changelog will be used in the release notes for the WooCommerce release. That's why it should only list the PRs that have WooCoomerce Core in the WooCommerce Visibility section of their description. Don't include changes available in the feature plugin or development builds. - - Run through the testing checklist to ensure everything works in that branch for that package bump. **Note:** Testing should ensure any features/new blocks that are supposed to be behind feature gating for the core merge of this package update are working as expected. - - Testing should include completing the [Smoke testing checklist](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/internal-developers/testing/smoke-testing.md). It's up to you to verify that those tests have been done. - - [ ] Verify and make any additional edits to the pull request description for things like: Changelog to be included with WooCommerce core, additional communication that might be needed elsewhere, additional marketing communication notes that may be needed, etc. - - [ ] Assign the corresponding WC version milestone to the PR - - [ ] After the checklist is complete and the testing is done, select the porter of your team to review the PR. Once approved, make sure you merge the PR. + + +- [ ] Build WC core from that branch with `pnpm run --filter='woocommerce' build ` (you might need to [install the dependencies first](https://github.com/woocommerce/woocommerce#prerequisites)) and: + + - [ ] Make sure the correct version of WC Blocks is being loaded. This can be done testing at least one of the testing steps from the release. + - [ ] Complete the [Smoke testing checklist](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/internal-developers/testing/smoke-testing.md). + +- [ ] Verify and make any additional edits to the pull request description for things like: Changelog to be included with WooCommerce core, additional communication that might be needed elsewhere, additional marketing communication notes that may be needed, etc. + - [ ] Assign the corresponding WC version milestone to the PR + - [ ] After the checklist is complete and the testing is done, select the porter of your team to review the PR. Once approved, make sure you merge the PR. - [ ] Make sure you join the `#woo-core-releases` Slack channel to represent Woo Blocks for the release of WooCommerce core this version is included in. - [ ] Search the release thread of the WooCommerce core version in WooCommerce P2 (example: p6q8Tx-2gl-p2). @@ -137,3 +142,4 @@ This only needs to be done if this release is the last release of the feature pl - [WCCOM product page](https://woocommerce.com/products/woocommerce-gutenberg-products-block/) - [WooCommerce blocks main documentation page](https://docs.woocommerce.com/document/woocommerce-blocks/) - [ ] Go through the description of the release pull request and edit it to update all the sections and checklist instructions there. +- [ ] Close this PR. diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json b/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json index 7d2b45b32a2..caebdacb115 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json +++ b/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json @@ -5,6 +5,7 @@ "description": "Display a button so the customer can add a product to their cart. Options will also be displayed depending on product type. e.g. quantity, variation.", "category": "woocommerce", "keywords": [ "WooCommerce" ], + "usesContext": ["postId"], "textdomain": "woo-gutenberg-products-block", "apiVersion": 2, "$schema": "https://schemas.wp.org/trunk/block.json" diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart-form/edit.tsx b/assets/js/atomic/blocks/product-elements/add-to-cart-form/edit.tsx index e8c5c92f8b0..39ed82bfcd0 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart-form/edit.tsx +++ b/assets/js/atomic/blocks/product-elements/add-to-cart-form/edit.tsx @@ -3,7 +3,9 @@ */ import { useBlockProps } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; -import { Button, Disabled, Notice } from '@wordpress/components'; +import { Button, Disabled, Tooltip } from '@wordpress/components'; +import { Skeleton } from '@woocommerce/base-components/skeleton'; + /** * Internal dependencies */ @@ -19,31 +21,34 @@ const Edit = () => { return (
- - -

- { __( - 'Customers will see product add-to-cart options displayed here, dependent on the product type.', - 'woo-gutenberg-products-block' - ) } -

-
- - -
+ +
+ + + + + +
+
); }; diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart-form/editor.scss b/assets/js/atomic/blocks/product-elements/add-to-cart-form/editor.scss index c45cef71e54..371171a8eff 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart-form/editor.scss +++ b/assets/js/atomic/blocks/product-elements/add-to-cart-form/editor.scss @@ -1,15 +1,10 @@ -.wc-block-add-to-cart-form { +.wc-block-editor-add-to-cart-form { display: flex; - flex-direction: row; + flex-direction: column; + row-gap: $default-block-margin; } -.wc-block-add-to-cart-form__notice.components-notice { - margin: 10px 0; - color: $black; - max-width: 60%; -} - -input.wc-block-add-to-cart-form__quantity[type="number"] { +input.wc-block-editor-add-to-cart-form__quantity[type="number"] { max-width: 50px; min-height: 23px; float: left; @@ -28,3 +23,10 @@ button.components-button.wc-block-add-to-cart-form__button { padding: 20px 30px; border-radius: 0; } + +.wc-block-editor-container { + cursor: help; + gap: 10px; + display: flex; + flex-direction: column; +} diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx b/assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx index 394c8c27e44..4443eae87c3 100644 --- a/assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx +++ b/assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; import { Icon, button } from '@wordpress/icons'; /** @@ -11,26 +10,24 @@ import { Icon, button } from '@wordpress/icons'; import metadata from './block.json'; import edit from './edit'; -registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - icon: { - src: ( - - ), - }, - edit, - save() { - return null; - }, - } ); +const blockSettings = { + edit, + icon: { + src: ( + + ), }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); + ancestor: [ 'woocommerce/single-product' ], + save() { + return null; }, +}; + +registerBlockSingleProductTemplate( { blockName: metadata.name, + blockMetadata: metadata, + blockSettings, } ); diff --git a/assets/js/atomic/blocks/product-elements/image/attributes.ts b/assets/js/atomic/blocks/product-elements/image/attributes.ts index a56235b6412..afadb4d15a9 100644 --- a/assets/js/atomic/blocks/product-elements/image/attributes.ts +++ b/assets/js/atomic/blocks/product-elements/image/attributes.ts @@ -28,6 +28,10 @@ export const blockAttributes: BlockAttributes = { type: 'boolean', default: false, }, + isDescendentOfSingleProductBlock: { + type: 'boolean', + default: false, + }, }; export default blockAttributes; diff --git a/assets/js/atomic/blocks/product-elements/price/attributes.ts b/assets/js/atomic/blocks/product-elements/price/attributes.ts index 98babfd82d8..0acba812f16 100644 --- a/assets/js/atomic/blocks/product-elements/price/attributes.ts +++ b/assets/js/atomic/blocks/product-elements/price/attributes.ts @@ -20,6 +20,10 @@ export const blockAttributes: BlockAttributes = { type: 'boolean', default: false, }, + isDescendentOfSingleProductBlock: { + type: 'boolean', + default: false, + }, }; export default blockAttributes; diff --git a/assets/js/atomic/blocks/product-elements/price/block.tsx b/assets/js/atomic/blocks/product-elements/price/block.tsx index 95020978e38..cb0daaa826f 100644 --- a/assets/js/atomic/blocks/product-elements/price/block.tsx +++ b/assets/js/atomic/blocks/product-elements/price/block.tsx @@ -54,7 +54,8 @@ export const Block = ( props: Props ): JSX.Element | null => { colorProps.className, { [ `${ parentClassName }__product-price` ]: parentClassName, - } + }, + typographyProps.className ); if ( ! product.id && ! isDescendentOfSingleProductTemplate ) { diff --git a/assets/js/atomic/blocks/product-elements/price/edit.tsx b/assets/js/atomic/blocks/product-elements/price/edit.tsx index 89f501d1fc3..b45238e9f36 100644 --- a/assets/js/atomic/blocks/product-elements/price/edit.tsx +++ b/assets/js/atomic/blocks/product-elements/price/edit.tsx @@ -27,6 +27,9 @@ interface BlockAttributes { interface Attributes { textAlign: 'left' | 'center' | 'right'; + isDescendentOfSingleProduct: boolean; + isDescendentOfSingleProductBlock: boolean; + productId: number; } interface Context { @@ -80,7 +83,9 @@ const PriceEdit = ( { ); const showProductSelector = - ! isDescendentOfQueryLoop && ! isDescendentOfSingleProductTemplate; + ! isDescendentOfQueryLoop && + ! isDescendentOfSingleProductTemplate && + ! attributes.isDescendentOfSingleProductBlock; if ( ! showProductSelector ) { return ( diff --git a/assets/js/atomic/blocks/product-elements/price/supports.ts b/assets/js/atomic/blocks/product-elements/price/supports.ts index cedd8b3c294..3cca5e36b1e 100644 --- a/assets/js/atomic/blocks/product-elements/price/supports.ts +++ b/assets/js/atomic/blocks/product-elements/price/supports.ts @@ -14,21 +14,25 @@ export const supports = { ...( isFeaturePluginBuild() && { color: { text: true, - background: false, + background: true, link: false, __experimentalSkipSerialization: true, }, typography: { fontSize: true, + lineHeight: true, + __experimentalFontFamily: true, __experimentalFontWeight: true, __experimentalFontStyle: true, __experimentalSkipSerialization: true, + __experimentalLetterSpacing: true, }, __experimentalSelector: '.wc-block-components-product-price', } ), ...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && { spacing: { margin: true, + padding: true, }, } ), }; diff --git a/assets/js/atomic/blocks/product-elements/product-details/index.tsx b/assets/js/atomic/blocks/product-elements/product-details/index.tsx index 6d2ec77578e..7dfa8e97bd4 100644 --- a/assets/js/atomic/blocks/product-elements/product-details/index.tsx +++ b/assets/js/atomic/blocks/product-elements/product-details/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -11,14 +10,11 @@ import metadata from './block.json'; import edit from './edit'; registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - edit, - } ); - }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); - }, blockName: metadata.name, + // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core + blockMetadata: metadata, + blockSettings: { + edit, + ancestor: [ 'woocommerce/single-product' ], + }, } ); diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx b/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx index df6fb8ed95e..b4f50c9ca6a 100644 --- a/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/edit.tsx @@ -6,6 +6,7 @@ import { isEmptyObject } from '@woocommerce/types'; import { useBlockProps } from '@wordpress/block-editor'; import { BlockAttributes } from '@wordpress/blocks'; import { Disabled } from '@wordpress/components'; +import type { BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies @@ -40,8 +41,7 @@ type Context = { queryId: string; }; -interface Props { - attributes: BlockAttributes; +interface Props extends BlockEditProps< BlockAttributes > { context: Context; } @@ -58,7 +58,7 @@ const Edit = ( { context }: Props ) => { ); } // We have work on this case when we will work on the Single Product block. - return ''; + return <>; }; export default Edit; diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss b/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss index 40696afdbba..5b04e305fd0 100644 --- a/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/editor.scss @@ -1,7 +1,8 @@ .wc-block-editor-product-gallery { img { - width: 500px; - height: 500px; + max-width: 500px; + width: 100%; + height: auto; } .wc-block-editor-product-gallery__other-images { img { diff --git a/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts b/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts index 9fccf6daff6..7c7d4c13dc0 100644 --- a/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts +++ b/assets/js/atomic/blocks/product-elements/product-image-gallery/index.ts @@ -2,7 +2,6 @@ * External dependencies */ import { gallery as icon } from '@wordpress/icons'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; /** @@ -13,15 +12,13 @@ import metadata from './block.json'; import './style.scss'; registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - icon, - edit, - } ); - }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); - }, blockName: metadata.name, + // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core + blockMetadata: metadata, + blockSettings: { + icon, + // @ts-expect-error `edit` can be extended to include other attributes + edit, + ancestor: [ 'woocommerce/single-product' ], + }, } ); diff --git a/assets/js/atomic/blocks/product-elements/product-meta/index.tsx b/assets/js/atomic/blocks/product-elements/product-meta/index.tsx index 1db84adc949..7144d879bb2 100644 --- a/assets/js/atomic/blocks/product-elements/product-meta/index.tsx +++ b/assets/js/atomic/blocks/product-elements/product-meta/index.tsx @@ -2,27 +2,21 @@ * External dependencies */ import { box as icon } from '@wordpress/icons'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; /** * Internal dependencies */ import edit from './edit'; -import save from './save'; import metadata from './block.json'; registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - icon, - edit, - save, - } ); - }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); - }, blockName: metadata.name, + // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core + blockMetadata: metadata, + blockSettings: { + edit, + icon, + ancestor: [ 'woocommerce/single-product' ], + }, } ); diff --git a/assets/js/atomic/blocks/product-elements/product-reviews/index.tsx b/assets/js/atomic/blocks/product-elements/product-reviews/index.tsx index 6d2ec77578e..7dfa8e97bd4 100644 --- a/assets/js/atomic/blocks/product-elements/product-reviews/index.tsx +++ b/assets/js/atomic/blocks/product-elements/product-reviews/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -11,14 +10,11 @@ import metadata from './block.json'; import edit from './edit'; registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - edit, - } ); - }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); - }, blockName: metadata.name, + // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core + blockMetadata: metadata, + blockSettings: { + edit, + ancestor: [ 'woocommerce/single-product' ], + }, } ); diff --git a/assets/js/atomic/blocks/product-elements/rating/attributes.ts b/assets/js/atomic/blocks/product-elements/rating/attributes.ts index 5d621390109..d8bc88bb321 100644 --- a/assets/js/atomic/blocks/product-elements/rating/attributes.ts +++ b/assets/js/atomic/blocks/product-elements/rating/attributes.ts @@ -16,6 +16,10 @@ export const blockAttributes: BlockAttributes = { type: 'string', default: '', }, + isDescendentOfSingleProductBlock: { + type: 'boolean', + default: false, + }, }; export default blockAttributes; diff --git a/assets/js/atomic/blocks/product-elements/rating/style.scss b/assets/js/atomic/blocks/product-elements/rating/style.scss index 5cd6f8e4191..870c4f2dfc0 100644 --- a/assets/js/atomic/blocks/product-elements/rating/style.scss +++ b/assets/js/atomic/blocks/product-elements/rating/style.scss @@ -1,5 +1,6 @@ .wc-block-components-product-rating { display: block; + line-height: 1; &__stars { display: inline-block; @@ -45,6 +46,7 @@ &__link { display: inline-block; + height: 1.618em; width: 100%; text-align: inherit; @include font-size(small); diff --git a/assets/js/atomic/blocks/product-elements/related-products/index.tsx b/assets/js/atomic/blocks/product-elements/related-products/index.tsx index 1db84adc949..192e4f7f661 100644 --- a/assets/js/atomic/blocks/product-elements/related-products/index.tsx +++ b/assets/js/atomic/blocks/product-elements/related-products/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { box as icon } from '@wordpress/icons'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; /** @@ -13,16 +12,13 @@ import save from './save'; import metadata from './block.json'; registerBlockSingleProductTemplate( { - registerBlockFn: () => { - // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. - registerBlockType( metadata, { - icon, - edit, - save, - } ); - }, - unregisterBlockFn: () => { - unregisterBlockType( metadata.name ); - }, blockName: metadata.name, + // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core + blockMetadata: metadata, + blockSettings: { + icon, + edit, + save, + ancestor: [ 'woocommerce/single-product' ], + }, } ); diff --git a/assets/js/atomic/blocks/product-elements/save.js b/assets/js/atomic/blocks/product-elements/save.js index 1033886fc94..c678c0d3557 100644 --- a/assets/js/atomic/blocks/product-elements/save.js +++ b/assets/js/atomic/blocks/product-elements/save.js @@ -6,6 +6,7 @@ import classnames from 'classnames'; const save = ( { attributes } ) => { if ( attributes.isDescendentOfQueryLoop || + attributes.isDescendentOfSingleProductBlock || attributes.isDescendentOfSingleProductTemplate ) { return null; diff --git a/assets/js/atomic/blocks/product-elements/sku/attributes.ts b/assets/js/atomic/blocks/product-elements/sku/attributes.ts index 80b6226b1dd..d70b39f1a99 100644 --- a/assets/js/atomic/blocks/product-elements/sku/attributes.ts +++ b/assets/js/atomic/blocks/product-elements/sku/attributes.ts @@ -16,6 +16,10 @@ export const blockAttributes: BlockAttributes = { type: 'boolean', default: false, }, + isDescendantOfAllProducts: { + type: 'boolean', + default: false, + }, showProductSelector: { type: 'boolean', default: false, diff --git a/assets/js/atomic/blocks/product-elements/sku/block.tsx b/assets/js/atomic/blocks/product-elements/sku/block.tsx index 33fad6fc911..6386d201c79 100644 --- a/assets/js/atomic/blocks/product-elements/sku/block.tsx +++ b/assets/js/atomic/blocks/product-elements/sku/block.tsx @@ -9,6 +9,11 @@ import { } from '@woocommerce/shared-context'; import { withProductDataContext } from '@woocommerce/shared-hocs'; import type { HTMLAttributes } from 'react'; +import { + useColorProps, + useSpacingProps, + useTypographyProps, +} from '@woocommerce/base-hooks'; /** * Internal dependencies @@ -22,15 +27,18 @@ const Preview = ( { parentClassName, sku, className, + style, }: { parentClassName: string; sku: string; className?: string | undefined; + style?: React.CSSProperties | undefined; } ) => (
{ __( 'SKU:', 'woo-gutenberg-products-block' ) }{ ' ' } { sku } @@ -43,6 +51,10 @@ const Block = ( props: Props ): JSX.Element | null => { const { product } = useProductDataContext(); const sku = product.sku; + const colorProps = useColorProps( props ); + const typographyProps = useTypographyProps( props ); + const spacingProps = useSpacingProps( props ); + if ( props.isDescendentOfSingleProductTemplate ) { return ( { className={ className } parentClassName={ parentClassName } sku={ sku } + { ...( props.isDescendantOfAllProducts && { + className: classnames( + className, + 'wc-block-components-product-sku wp-block-woocommerce-product-sku', + { + [ colorProps.className ]: colorProps.className, + [ typographyProps.className ]: + typographyProps.className, + } + ), + style: { + ...colorProps.style, + ...typographyProps.style, + ...spacingProps.style, + }, + } ) } /> ); }; diff --git a/assets/js/atomic/blocks/product-elements/sku/edit.tsx b/assets/js/atomic/blocks/product-elements/sku/edit.tsx index db1e0ae340e..34b60f74397 100644 --- a/assets/js/atomic/blocks/product-elements/sku/edit.tsx +++ b/assets/js/atomic/blocks/product-elements/sku/edit.tsx @@ -18,7 +18,10 @@ const Edit = ( { setAttributes, context, }: BlockEditProps< Attributes > & { context: Context } ): JSX.Element => { - const blockProps = useBlockProps(); + const { style, ...blockProps } = useBlockProps( { + className: + 'wc-block-components-product-sku wp-block-woocommerce-product-sku', + } ); const blockAttrs = { ...attributes, ...context, @@ -31,10 +34,22 @@ const Edit = ( { ); return ( -
+ <> - -
+
+ +
+ ); }; diff --git a/assets/js/atomic/blocks/product-elements/sku/index.tsx b/assets/js/atomic/blocks/product-elements/sku/index.tsx index 46f314ede8e..a623e6db717 100644 --- a/assets/js/atomic/blocks/product-elements/sku/index.tsx +++ b/assets/js/atomic/blocks/product-elements/sku/index.tsx @@ -10,6 +10,7 @@ import type { BlockConfiguration } from '@wordpress/blocks'; import sharedConfig from '../shared/config'; import attributes from './attributes'; import edit from './edit'; +import { supports } from './supports'; import { BLOCK_TITLE as title, BLOCK_ICON as icon, @@ -33,6 +34,7 @@ const blockConfig: BlockConfiguration = { 'woocommerce/product-meta', ], edit, + supports, }; registerBlockType( 'woocommerce/product-sku', { ...blockConfig } ); diff --git a/assets/js/atomic/blocks/product-elements/sku/style.scss b/assets/js/atomic/blocks/product-elements/sku/style.scss index 4cc0eb991fb..d32bc9ca521 100644 --- a/assets/js/atomic/blocks/product-elements/sku/style.scss +++ b/assets/js/atomic/blocks/product-elements/sku/style.scss @@ -2,4 +2,5 @@ display: block; text-transform: uppercase; @include font-size(small); + overflow-wrap: break-word; } diff --git a/assets/js/atomic/blocks/product-elements/sku/supports.ts b/assets/js/atomic/blocks/product-elements/sku/supports.ts new file mode 100644 index 00000000000..6c6f9120303 --- /dev/null +++ b/assets/js/atomic/blocks/product-elements/sku/supports.ts @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { isFeaturePluginBuild } from '@woocommerce/block-settings'; +import { + // @ts-expect-error We check if this exists before using it. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalGetSpacingClassesAndStyles, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import sharedConfig from '../shared/config'; + +export const supports = { + ...sharedConfig.supports, + color: { + text: true, + background: true, + }, + typography: { + fontSize: true, + lineHeight: true, + ...( isFeaturePluginBuild() && { + __experimentalFontWeight: true, + __experimentalFontFamily: true, + __experimentalFontStyle: true, + __experimentalTextTransform: true, + __experimentalTextDecoration: true, + __experimentalLetterSpacing: true, + } ), + }, + ...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && { + spacing: { + margin: true, + padding: true, + }, + } ), +}; diff --git a/assets/js/atomic/blocks/product-elements/sku/types.ts b/assets/js/atomic/blocks/product-elements/sku/types.ts index 9651b739436..e4429dcd9c7 100644 --- a/assets/js/atomic/blocks/product-elements/sku/types.ts +++ b/assets/js/atomic/blocks/product-elements/sku/types.ts @@ -3,4 +3,5 @@ export interface Attributes { isDescendentOfQueryLoop: boolean; isDescendentOfSingleProductTemplate: boolean; showProductSelector: boolean; + isDescendantOfAllProducts: boolean; } diff --git a/assets/js/atomic/utils/register-block-single-product-template.ts b/assets/js/atomic/utils/register-block-single-product-template.ts index 675e335b06d..8b7e492f751 100644 --- a/assets/js/atomic/utils/register-block-single-product-template.ts +++ b/assets/js/atomic/utils/register-block-single-product-template.ts @@ -1,46 +1,104 @@ /** * External dependencies */ -import { getBlockType } from '@wordpress/blocks'; +import { + BlockAttributes, + BlockConfiguration, + BlockVariation, + getBlockType, + registerBlockType, + registerBlockVariation, + unregisterBlockType, + unregisterBlockVariation, +} from '@wordpress/blocks'; import { subscribe, select } from '@wordpress/data'; export const registerBlockSingleProductTemplate = ( { - registerBlockFn, - unregisterBlockFn, blockName, + blockMetadata, + blockSettings, + isVariationBlock = false, + variationName, }: { - registerBlockFn: () => void; - unregisterBlockFn: () => void; blockName: string; + blockMetadata: Partial< BlockConfiguration >; + blockSettings: Partial< BlockConfiguration >; + isVariationBlock?: boolean; + variationName?: string; } ) => { - let currentTemplateId: string | undefined; + let currentTemplateId: string | undefined = ''; subscribe( () => { const previousTemplateId = currentTemplateId; const store = select( 'core/edit-site' ); - currentTemplateId = store?.getEditedPostId() as string | undefined; + currentTemplateId = store?.getEditedPostContext< { + templateSlug?: string; + } >()?.templateSlug; + const hasChangedTemplate = previousTemplateId !== currentTemplateId; + const hasTemplateId = Boolean( currentTemplateId ); - if ( previousTemplateId === currentTemplateId ) { + if ( ! hasChangedTemplate || ! hasTemplateId || ! blockName ) { return; } - const parsedTemplate = currentTemplateId?.split( '//' )[ 1 ]; - - if ( parsedTemplate === null || parsedTemplate === undefined ) { - return; - } - - const block = getBlockType( blockName ); + let isBlockRegistered = Boolean( getBlockType( blockName ) ); + /** + * We need to unregister the block each time the user visits or leaves the Single Product template. + * + * The Single Product template is the only template where the `ancestor` property is not needed because it provides the context + * for the product blocks. We need to unregister and re-register the block to remove or add the `ancestor` property depending on which + * location (template, post, page, etc.) the user is in. + * + */ if ( - block === undefined && - parsedTemplate.includes( 'single-product' ) + isBlockRegistered && + ( currentTemplateId?.includes( 'single-product' ) || + previousTemplateId?.includes( 'single-product' ) ) ) { - registerBlockFn(); + if ( isVariationBlock && variationName ) { + unregisterBlockVariation( blockName, variationName ); + } else { + unregisterBlockType( blockName ); + } + isBlockRegistered = false; } - if ( block !== undefined ) { - unregisterBlockFn(); + if ( ! isBlockRegistered ) { + if ( isVariationBlock ) { + registerBlockVariation( blockName, { + ...blockSettings, + // @ts-expect-error: `ancestor` key is typed in WordPress core + ancestor: ! currentTemplateId?.includes( 'single-product' ) + ? blockSettings?.ancestor + : undefined, + } ); + } else { + // @ts-expect-error: `registerBlockType` is typed in WordPress core + registerBlockType( blockMetadata, { + ...blockSettings, + ancestor: ! currentTemplateId?.includes( 'single-product' ) + ? blockSettings?.ancestor + : undefined, + } ); + } } }, 'core/edit-site' ); + + subscribe( () => { + const isBlockRegistered = Boolean( getBlockType( blockName ) ); + const editPostStoreExists = Boolean( select( 'core/edit-post' ) ); + + if ( ! isBlockRegistered && editPostStoreExists ) { + if ( isVariationBlock ) { + registerBlockVariation( + blockName, + blockSettings as BlockVariation< BlockAttributes > + ); + } else { + // @ts-expect-error: `registerBlockType` is typed in WordPress core + registerBlockType( blockMetadata, blockSettings ); + } + } + }, 'core/edit-post' ); }; diff --git a/assets/js/base/components/cart-checkout/pickup-location/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/index.tsx new file mode 100644 index 00000000000..a213f795f71 --- /dev/null +++ b/assets/js/base/components/cart-checkout/pickup-location/index.tsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { isObject, objectHasProp } from '@woocommerce/types'; +import { isPackageRateCollectable } from '@woocommerce/base-utils'; + +/** + * Shows a formatted pickup location. + */ +const PickupLocation = (): JSX.Element | null => { + const { pickupAddress, pickupMethod } = useSelect( ( select ) => { + const cartShippingRates = select( 'wc/store/cart' ).getShippingRates(); + + const flattenedRates = cartShippingRates.flatMap( + ( cartShippingRate ) => cartShippingRate.shipping_rates + ); + const selectedCollectableRate = flattenedRates.find( + ( rate ) => rate.selected && isPackageRateCollectable( rate ) + ); + + // If the rate has an address specified in its metadata. + if ( + isObject( selectedCollectableRate ) && + objectHasProp( selectedCollectableRate, 'meta_data' ) + ) { + const selectedRateMetaData = selectedCollectableRate.meta_data.find( + ( meta ) => meta.key === 'pickup_address' + ); + if ( + isObject( selectedRateMetaData ) && + objectHasProp( selectedRateMetaData, 'value' ) && + selectedRateMetaData.value + ) { + const selectedRatePickupAddress = selectedRateMetaData.value; + return { + pickupAddress: selectedRatePickupAddress, + pickupMethod: selectedCollectableRate.name, + }; + } + } + + if ( isObject( selectedCollectableRate ) ) { + return { + pickupAddress: undefined, + pickupMethod: selectedCollectableRate.name, + }; + } + return { + pickupAddress: undefined, + pickupMethod: undefined, + }; + } ); + + // If the method does not contain an address, or the method supporting collection was not found, return early. + if ( + typeof pickupAddress === 'undefined' && + typeof pickupMethod === 'undefined' + ) { + return null; + } + + // Show the pickup method's name if we don't have an address to show. + return ( + + { sprintf( + /* translators: %s: shipping method name, e.g. "Amazon Locker" */ + __( 'Collection from %s', 'woo-gutenberg-products-block' ), + typeof pickupAddress === 'undefined' + ? pickupMethod + : pickupAddress + ) + ' ' } + + ); +}; + +export default PickupLocation; diff --git a/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx new file mode 100644 index 00000000000..603bb951931 --- /dev/null +++ b/assets/js/base/components/cart-checkout/pickup-location/test/index.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { dispatch } from '@wordpress/data'; +import { previewCart } from '@woocommerce/resource-previews'; +import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; + +jest.mock( '@woocommerce/settings', () => { + const originalModule = jest.requireActual( '@woocommerce/settings' ); + + return { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore We know @woocommerce/settings is an object. + ...originalModule, + getSetting: ( setting: string, ...rest: unknown[] ) => { + if ( setting === 'localPickupEnabled' ) { + return true; + } + if ( setting === 'collectableMethodIds' ) { + return [ 'pickup_location' ]; + } + return originalModule.getSetting( setting, ...rest ); + }, + }; +} ); +describe( 'PickupLocation', () => { + it( `renders an address if one is set in the method's metadata`, async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.method_id === 'pickup_location' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( ); + expect( + screen.getByText( + /Collection from 123 Easy Street, New York, 12345/ + ) + ).toBeInTheDocument(); + } ); + it( 'renders the method name if address is not in metadata', async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.rate_id === 'pickup_location:2' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + // Set the pickup_location metadata value to an empty string in the selected pickup rate. + const addressKeyIndex = previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data.findIndex( + ( metaData ) => metaData.key === 'pickup_address' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].meta_data[ addressKeyIndex ].value = ''; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( ); + expect( + screen.getByText( /Collection from Local pickup/ ) + ).toBeInTheDocument(); + } ); +} ); diff --git a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx index bd4970e16f5..b735a1724c8 100644 --- a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx +++ b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx @@ -4,10 +4,7 @@ import { __ } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; -import { - ExperimentalOrderShippingPackages, - StoreNotice, -} from '@woocommerce/blocks-checkout'; +import { ExperimentalOrderShippingPackages } from '@woocommerce/blocks-checkout'; import { getShippingRatesPackageCount, getShippingRatesRateCount, @@ -17,6 +14,7 @@ import { useEditorContext, useShippingData, } from '@woocommerce/base-context'; +import NoticeBanner from '@woocommerce/base-components/notice-banner'; /** * Internal dependencies @@ -111,7 +109,7 @@ const ShippingRatesControl = ( { { hasSelectedLocalPickup && shippingRates.length > 1 && ! isEditor && ( - + ) } { - const checkoutStore = select( CHECKOUT_STORE_KEY ); - return { - prefersCollection: checkoutStore.prefersCollection(), - }; - } ); const totalShippingValue = getTotalShippingValue( values ); const hasRates = hasShippingRate( shippingRates ) || totalShippingValue > 0; const showShippingCalculatorForm = showCalculator && isShippingCalculatorOpen; + const prefersCollection = useSelect( ( select ) => { + return select( CHECKOUT_STORE_KEY ).prefersCollection(); + } ); const selectedShippingRates = shippingRates.flatMap( ( shippingPackage ) => { return shippingPackage.shipping_rates - .filter( ( rate ) => rate.selected ) + .filter( + ( rate ) => + // If the shopper prefers collection, the rate is collectable AND selected. + ( prefersCollection && + isPackageRateCollectable( rate ) && + rate.selected ) || + // Or the shopper does not prefer collection and the rate is selected + ( ! prefersCollection && rate.selected ) + ) .flatMap( ( rate ) => rate.name ); } ); @@ -104,18 +111,16 @@ export const TotalsShipping = ( { - { ! prefersCollection && ( - - ) } + ) : null } diff --git a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx index c2be8f15954..2d95810aa88 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx @@ -2,12 +2,15 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { EnteredAddress } from '@woocommerce/settings'; import { formatShippingAddress, isAddressComplete, } from '@woocommerce/base-utils'; import { useEditorContext } from '@woocommerce/base-context'; +import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings'; +import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -19,7 +22,7 @@ export interface ShippingAddressProps { showCalculator: boolean; isShippingCalculatorOpen: boolean; setIsShippingCalculatorOpen: CalculatorButtonProps[ 'setIsShippingCalculatorOpen' ]; - shippingAddress: EnteredAddress; + shippingAddress: ShippingAddressType; } export const ShippingAddress = ( { @@ -30,7 +33,9 @@ export const ShippingAddress = ( { }: ShippingAddressProps ): JSX.Element | null => { const addressComplete = isAddressComplete( shippingAddress ); const { isEditor } = useEditorContext(); - + const prefersCollection = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).prefersCollection() + ); // If the address is incomplete, and we're not in the editor, don't show anything. if ( ! addressComplete && ! isEditor ) { return null; @@ -38,8 +43,12 @@ export const ShippingAddress = ( { const formattedLocation = formatShippingAddress( shippingAddress ); return ( <> - - { showCalculator && ( + { prefersCollection ? ( + + ) : ( + + ) } + { showCalculator && ! prefersCollection ? ( - ) } + ) : null } ); }; diff --git a/assets/js/base/components/cart-checkout/totals/shipping/shipping-rate-selector.tsx b/assets/js/base/components/cart-checkout/totals/shipping/shipping-rate-selector.tsx index a6bbe6dcdad..099b279c7b8 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/shipping-rate-selector.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/shipping-rate-selector.tsx @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import type { CartResponseShippingRate } from '@woocommerce/types'; +import NoticeBanner from '@woocommerce/base-components/notice-banner'; /** * Internal dependencies @@ -32,11 +33,18 @@ export const ShippingRateSelector = ( { className="wc-block-components-totals-shipping__options" noResultsMessage={ <> - { isAddressComplete && - __( - 'There are no shipping options available. Please check your shipping address.', - 'woo-gutenberg-products-block' - ) } + { isAddressComplete && ( + + { __( + 'There are no shipping options available. Please check your shipping address.', + 'woo-gutenberg-products-block' + ) } + + ) } } shippingRates={ shippingRates } diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx index daa43f4a967..ed04a2eb123 100644 --- a/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/index.tsx @@ -18,9 +18,24 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); -wpData.useSelect.mockImplementation( () => { - return { prefersCollection: false }; -} ); +// Mock use select so we can override it when wc/store/checkout is accessed, but return the original select function if any other store is accessed. +wpData.useSelect.mockImplementation( + jest.fn().mockImplementation( ( passedMapSelect ) => { + const mockedSelect = jest.fn().mockImplementation( ( storeName ) => { + if ( storeName === 'wc/store/checkout' ) { + return { + prefersCollection() { + return false; + }, + }; + } + return jest.requireActual( '@wordpress/data' ).select( storeName ); + } ); + passedMapSelect( mockedSelect, { + dispatch: jest.requireActual( '@wordpress/data' ).dispatch, + } ); + } ) +); const shippingAddress = { first_name: 'John', diff --git a/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx new file mode 100644 index 00000000000..c3be82cf4cd --- /dev/null +++ b/assets/js/base/components/cart-checkout/totals/shipping/test/shipping-address.tsx @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import ShippingAddress from '@woocommerce/base-components/cart-checkout/totals/shipping/shipping-address'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { dispatch } from '@wordpress/data'; +import { previewCart } from '@woocommerce/resource-previews'; + +jest.mock( '@woocommerce/settings', () => { + const originalModule = jest.requireActual( '@woocommerce/settings' ); + + return { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore We know @woocommerce/settings is an object. + ...originalModule, + getSetting: ( setting: string, ...rest: unknown[] ) => { + if ( setting === 'localPickupEnabled' ) { + return true; + } + if ( setting === 'collectableMethodIds' ) { + return [ 'pickup_location' ]; + } + return originalModule.getSetting( setting, ...rest ); + }, + }; +} ); +describe( 'ShippingAddress', () => { + const testShippingAddress = { + first_name: 'John', + last_name: 'Doe', + company: 'Automattic', + address_1: '123 Main St', + address_2: '', + city: 'San Francisco', + state: 'CA', + postcode: '94107', + country: 'US', + phone: '555-555-5555', + }; + + it( 'renders ShippingLocation if user does not prefer collection', () => { + render( + + ); + expect( screen.getByText( /Shipping to 94107/ ) ).toBeInTheDocument(); + expect( + screen.queryByText( /Collection from/ ) + ).not.toBeInTheDocument(); + } ); + it( 'renders PickupLocation if shopper prefers collection', async () => { + dispatch( CHECKOUT_STORE_KEY ).setPrefersCollection( true ); + + // Deselect the default selected rate and select pickup_location:1 rate. + const currentlySelectedIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.selected + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + currentlySelectedIndex + ].selected = false; + const pickupRateIndex = + previewCart.shipping_rates[ 0 ].shipping_rates.findIndex( + ( rate ) => rate.method_id === 'pickup_location' + ); + previewCart.shipping_rates[ 0 ].shipping_rates[ + pickupRateIndex + ].selected = true; + + dispatch( CART_STORE_KEY ).receiveCart( previewCart ); + + render( + + ); + expect( + screen.getByText( + /Collection from 123 Easy Street, New York, 12345/ + ) + ).toBeInTheDocument(); + } ); +} ); diff --git a/assets/js/base/components/formatted-monetary-amount/index.tsx b/assets/js/base/components/formatted-monetary-amount/index.tsx index f13aa03da01..f4a5e97157a 100644 --- a/assets/js/base/components/formatted-monetary-amount/index.tsx +++ b/assets/js/base/components/formatted-monetary-amount/index.tsx @@ -37,7 +37,6 @@ const currencyToNumberFormat = ( return { thousandSeparator: currency?.thousandSeparator, decimalSeparator: currency?.decimalSeparator, - decimalScale: currency?.minorUnit, fixedDecimalScale: true, prefix: currency?.prefix, suffix: currency?.suffix, @@ -83,9 +82,11 @@ const FormattedMonetaryAmount = ( { 'wc-block-components-formatted-money-amount', className ); + const decimalScale = props.decimalScale ?? currency?.minorUnit; const numberFormatProps = { ...props, ...currencyToNumberFormat( currency ), + decimalScale, value: undefined, currency: undefined, onValueChange: undefined, diff --git a/assets/js/base/components/index.ts b/assets/js/base/components/index.ts new file mode 100644 index 00000000000..aa445113079 --- /dev/null +++ b/assets/js/base/components/index.ts @@ -0,0 +1,40 @@ +export * from './block-error-boundary'; +export * from './button'; +export * from './cart-checkout'; +export * from './checkbox-list'; +export * from './chip'; +export * from './combobox'; +export * from './country-input'; +export * from './drawer'; +export * from './filter-element-label'; +export * from './filter-placeholder'; +export * from './filter-reset-button'; +export * from './filter-submit-button'; +export * from './form'; +export * from './form-token-field'; +export * from './formatted-monetary-amount'; +export * from './label'; +export * from './load-more-button'; +export * from './loading-mask'; +export * from './noninteractive'; +export * from './notice-banner'; +export * from './pagination'; +export * from './price-slider'; +export * from './product-list'; +export * from './product-name'; +export * from './product-price'; +export * from './product-rating'; +export * from './quantity-selector'; +export * from './radio-control'; +export * from './radio-control-accordion'; +export * from './read-more'; +export * from './reviews'; +export * from './sidebar-layout'; +export * from './snackbar-list'; +export * from './sort-select'; +export * from './spinner'; +export * from './state-input'; +export * from './summary'; +export * from './tabs'; +export * from './textarea'; +export * from './title'; diff --git a/assets/js/base/components/notice-banner/README.md b/assets/js/base/components/notice-banner/README.md new file mode 100644 index 00000000000..9345d194672 --- /dev/null +++ b/assets/js/base/components/notice-banner/README.md @@ -0,0 +1,138 @@ +# NoticeBanner Component + +An informational UI displayed near the top of the store pages. + +## Table of contents + +- [Design Guidelines](#design-guidelines) +- [Development Guidelines](#development-guidelines) + - [Usage](#usage) + - [Props](#props) + - [`children`: `React.ReactNode`](#children-reactreactnode) + - [`className`: `string`](#classname-string) + - [`isDismissible`: `boolean`](#isdismissible-boolean) + - [`onRemove`: `() => void`](#onremove---void) + - [`politeness`: `'polite' | 'assertive'`](#politeness-polite--assertive) + - [`spokenMessage`: `string`](#spokenmessage-string) + - [`status`: `'success' | 'error' | 'info' | 'warning' | 'default'`](#status-success--error--info--warning--default) + - [`summary`: `string`](#summary-string) + - [Example](#example) + +## Design Guidelines + +Notices are informational UI displayed near the top of store pages. Notices are used to indicate the result of an action, or to draw the user’s attention to necessary information. + +Notices are color-coded to indicate the type of message being communicated, and also show an icon to reinforce the meaning of the message. The color and icon used for a notice are determined by the `status` prop. + +### Informational + +Blue notices used for general information for buyers that are not blocking and do not require action. + +![Informational notice](./screenshots/info.png) + +### Error + +Red notices to show that an error has occurred and that the user needs to take action. + +![Error notice](./screenshots/error.png) + +### Success + +Green notices that show an action was successful. + +![Success notice](./screenshots/success.png) + +### Warning + +Yellow notices that show that the user may need to take action, or needs to be aware of something important. + +![Warning notice](./screenshots/warning.png) + +### Default + +Gray notice, similar to info, but used for less important messaging. + +![Default notice](./screenshots/default.png) + +## Development Guidelines + +### Usage + +To display a plain notice, pass the notice message as a string: + +```jsx +import { NoticeBanner } from '@woocommerce/base-components'; + +Your message here; +``` + +For more complex markup, you can pass any JSX element: + +```jsx +import { NoticeBanner } from '@woocommerce/base-components'; + + +

+ An error occurred: { errorDetails }. +

+
; +``` + +### Props + +#### `children`: `React.ReactNode` + +The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message. + +#### `className`: `string` + +Additional class name to give to the notice. + +#### `isDismissible`: `boolean` + +Determines whether the notice can be dismissed by the user. When set to true, a close icon will be displayed on the banner. + +#### `onRemove`: `() => void` + +Function called when dismissing the notice. When the close icon is clicked or the Escape key is pressed, this function will be called. + +#### `politeness`: `'polite' | 'assertive'` + +Determines the level of politeness for the notice for assistive technology. Acceptable values are 'polite' and 'assertive'. Default is 'polite'. + +#### `spokenMessage`: `string` + +Optionally provided to change the spoken message for assistive technology. If not provided, the `children` prop will be used as the spoken message. + +#### `status`: `'success' | 'error' | 'info' | 'warning' | 'default'` + +Status determines the color of the notice and the icon. Acceptable values are `success`, `error`, `info`, `warning`, and `default`. + +#### `summary`: `string` + +Optional summary text shown above notice content, used when several notices are listed together. + +##### Example + +```tsx +import { NoticeBanner } from '@woocommerce/base-components'; + +const errorMessages = [ + 'First error message', + 'Second error message', + 'Third error message', +]; + + +
    + { errorMessages.map( ( message ) => ( +
  • { message }
  • + ) ) } +
+
; +``` + +In this example, the summary prop is used to indicate to the user that there are errors in the form submission. The list of error messages is rendered within the NoticeBanner component using an unordered list (`
    `) and list items (`
  • `). The `status` prop is set to `error` to indicate that the notice represents an error message. diff --git a/assets/js/base/components/notice-banner/index.tsx b/assets/js/base/components/notice-banner/index.tsx new file mode 100644 index 00000000000..50dcbac641b --- /dev/null +++ b/assets/js/base/components/notice-banner/index.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { __ } from '@wordpress/i18n'; +import { Icon, close } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { getDefaultPoliteness, getStatusIcon } from './utils'; +import Button from '../button'; +import { useSpokenMessage } from '../../hooks'; + +export interface NoticeBannerProps { + // The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message. + children: React.ReactNode; + // Additional class name to give to the notice. + className?: string | undefined; + // Determines whether the notice can be dismissed by the user. + isDismissible?: boolean | undefined; + // Function called when dismissing the notice. + onRemove?: ( () => void ) | undefined; + // Determines the level of politeness for the notice for assistive technology. + politeness?: 'polite' | 'assertive' | undefined; + // Optionally provided to change the spoken message for assistive technology. + spokenMessage?: string | React.ReactNode | undefined; + // Status determines the color of the notice and the icon. + status: 'success' | 'error' | 'info' | 'warning' | 'default'; + // Optional summary text shown above notice content, used when several notices are listed together. + summary?: string | undefined; +} + +/** + * NoticeBanner: An informational UI displayed near the top of the store pages. + * + * Notices are informational UI displayed near the top of store pages. WooCommerce blocks, themes, and plugins all use + * notices to indicate the result of an action, or to draw the user’s attention to necessary information. + */ +const NoticeBanner = ( { + className, + status = 'default', + children, + spokenMessage = children, + onRemove = () => void 0, + isDismissible = true, + politeness = getDefaultPoliteness( status ), + summary, +}: NoticeBannerProps ) => { + useSpokenMessage( spokenMessage, politeness ); + + const dismiss = ( event: React.SyntheticEvent ) => { + if ( + typeof event?.preventDefault === 'function' && + event.preventDefault + ) { + event.preventDefault(); + } + onRemove(); + }; + + return ( +
    + +
    + { summary && ( +

    + { summary } +

    + ) } + { children } +
    + { !! isDismissible && ( +
    + ); +}; + +export default NoticeBanner; diff --git a/assets/js/base/components/notice-banner/screenshots/default.png b/assets/js/base/components/notice-banner/screenshots/default.png new file mode 100644 index 00000000000..b332186648e Binary files /dev/null and b/assets/js/base/components/notice-banner/screenshots/default.png differ diff --git a/assets/js/base/components/notice-banner/screenshots/error.png b/assets/js/base/components/notice-banner/screenshots/error.png new file mode 100644 index 00000000000..4de65910933 Binary files /dev/null and b/assets/js/base/components/notice-banner/screenshots/error.png differ diff --git a/assets/js/base/components/notice-banner/screenshots/info.png b/assets/js/base/components/notice-banner/screenshots/info.png new file mode 100644 index 00000000000..de4ebc5124b Binary files /dev/null and b/assets/js/base/components/notice-banner/screenshots/info.png differ diff --git a/assets/js/base/components/notice-banner/screenshots/success.png b/assets/js/base/components/notice-banner/screenshots/success.png new file mode 100644 index 00000000000..dcb4536ee19 Binary files /dev/null and b/assets/js/base/components/notice-banner/screenshots/success.png differ diff --git a/assets/js/base/components/notice-banner/screenshots/warning.png b/assets/js/base/components/notice-banner/screenshots/warning.png new file mode 100644 index 00000000000..8fcf176155b Binary files /dev/null and b/assets/js/base/components/notice-banner/screenshots/warning.png differ diff --git a/assets/js/base/components/notice-banner/stories/index.tsx b/assets/js/base/components/notice-banner/stories/index.tsx new file mode 100644 index 00000000000..202a0088e5e --- /dev/null +++ b/assets/js/base/components/notice-banner/stories/index.tsx @@ -0,0 +1,107 @@ +/** + * External dependencies + */ +import type { Story, Meta } from '@storybook/react'; + +/** + * Internal dependencies + */ +import NoticeBanner, { NoticeBannerProps } from '../'; +const availableStatus = [ 'default', 'success', 'error', 'warning', 'info' ]; + +export default { + title: 'WooCommerce Blocks/@base-components/NoticeBanner', + argTypes: { + status: { + control: 'radio', + options: availableStatus, + description: + 'Status determines the color of the notice and the icon.', + }, + isDismissible: { + control: 'boolean', + description: 'Determines whether the notice can be dismissed.', + }, + summary: { + description: + 'Optional summary text shown above notice content, used when several notices are listed together.', + control: 'text', + }, + className: { + description: 'Additional class name to give to the notice.', + control: 'text', + }, + spokenMessage: { + description: + 'Optionally provided to change the spoken message for assistive technology.', + control: 'text', + }, + politeness: { + control: 'radio', + options: [ 'polite', 'assertive' ], + description: + 'Determines the level of politeness for the notice for assistive technology.', + }, + children: { + description: + 'The content of the notice; either text or a React node such as a list of errors.', + disable: true, + }, + onRemove: { + description: 'Function called when dismissing the notice.', + disable: true, + }, + }, + component: NoticeBanner, +} as Meta< NoticeBannerProps >; + +const Template: Story< NoticeBannerProps > = ( args ) => { + return ; +}; + +export const Default = Template.bind( {} ); +Default.args = { + children: 'This is a default notice', + status: 'default', + isDismissible: true, + summary: undefined, + className: undefined, + spokenMessage: undefined, + politeness: undefined, +}; + +export const Error = Template.bind( {} ); +Error.args = { + children: 'This is an error notice', + status: 'error', +}; + +export const Warning = Template.bind( {} ); +Warning.args = { + children: 'This is a warning notice', + status: 'warning', +}; + +export const Info = Template.bind( {} ); +Info.args = { + children: 'This is an info notice', + status: 'info', +}; + +export const Success = Template.bind( {} ); +Success.args = { + children: 'This is a success notice', + status: 'success', +}; + +export const ErrorSummary = Template.bind( {} ); +ErrorSummary.args = { + summary: 'Please fix the following errors', + children: ( +
      +
    • This is an error notice
    • +
    • This is another error notice
    • +
    + ), + status: 'error', +}; diff --git a/assets/js/base/components/notice-banner/style.scss b/assets/js/base/components/notice-banner/style.scss new file mode 100644 index 00000000000..395de052cb4 --- /dev/null +++ b/assets/js/base/components/notice-banner/style.scss @@ -0,0 +1,149 @@ +%notice-banner { + display: flex; + align-items: stretch; + align-content: flex-start; + color: $gray-800; + padding: $gap !important; + gap: $gap-small; + margin: $gap 0; + border-radius: 4px; + border-color: $gray-800; + font-weight: 400; + line-height: 1.5; + border: 1px solid; + @include font-size(small); + background-color: #fff; + box-sizing: border-box; + + > .wc-block-components-notice-banner__content { + padding-right: $gap; + align-self: center; + white-space: normal; + flex-basis: 100%; + + &:last-child { + padding-right: 0; + } + + .wc-block-components-notice-banner__summary { + margin: 0 0 $gap-smaller; + font-weight: 600; + } + + ul, + ol { + margin: 0 0 0 $gap-large; + padding: 0; + + li::after { + content: ""; + clear: both; + display: block; + } + } + + // Legacy notice compatibility. + .wc-forward.wp-element-button { + float: right; + color: $gray-800 !important; + background: transparent; + padding: 0 !important; + margin: 0; + border: 0; + appearance: none; + opacity: 0.6; + + &:hover, + &:focus, + &:active { + opacity: 1; + } + } + } + + > svg { + fill: #fff; + border-radius: 50%; + padding: 2px; + background-color: $gray-800; + flex-shrink: 0; + flex-grow: 0; + } + + > .wc-block-components-button { + margin: 6px 0 0 auto !important; + background: transparent none !important; + box-shadow: none !important; + outline: none !important; + border: 0 !important; + padding: 0 !important; + height: 16px !important; + width: 16px !important; + min-height: auto !important; + color: $gray-800 !important; + min-width: 0 !important; + flex: 0 0 16px; + opacity: 0.6; + + > svg { + margin: 0 !important; + } + + &:hover, + &:focus, + &:active { + opacity: 1; + } + } +} + +%error { + border-color: $alert-red; + background-color: #fff0f0; + + > svg { + background-color: $alert-red; + transform: rotate(180deg); + } +} +%warning { + border-color: $alert-yellow; + background-color: #fffbf4; + + > svg { + background-color: $alert-yellow; + transform: rotate(180deg); + } +} +%success { + border-color: $alert-green; + background-color: #f4fff7; + + > svg { + background-color: $alert-green; + } +} +%info { + border-color: #007cba; + background-color: #f4f8ff; + + > svg { + background-color: #007cba; + } +} + +.wc-block-components-notice-banner { + @extend %notice-banner; + &.is-error { + @extend %error; + } + &.is-warning { + @extend %warning; + } + &.is-success { + @extend %success; + } + &.is-info { + @extend %info; + } +} diff --git a/assets/js/base/components/notice-banner/test/index.tsx b/assets/js/base/components/notice-banner/test/index.tsx new file mode 100644 index 00000000000..ce9f2722cf6 --- /dev/null +++ b/assets/js/base/components/notice-banner/test/index.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import { render, fireEvent, findByText } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import NoticeBanner from '../index'; + +describe( 'NoticeBanner', () => { + test( 'renders without errors when all required props are provided', async () => { + const { container } = render( + This is an error message + ); + expect( + await findByText( container, 'This is an error message' ) + ).toBeInTheDocument(); + } ); + + test( 'displays the notice message correctly', () => { + const message = 'This is a test message'; + const { getByText } = render( + + { message } + + ); + const messageElement = getByText( message ); + expect( messageElement ).toBeInTheDocument(); + } ); + + test( 'displays the correct status for the notice', () => { + const { container } = render( + + This is a warning message + + ); + expect( container.querySelector( '.is-warning' ) ).toBeInTheDocument(); + } ); + + test( 'displays the summary correctly when provided', () => { + const summaryText = '4 new messages'; + const { getByText } = render( + + This is a test message + + ); + const summaryElement = getByText( summaryText ); + expect( summaryElement ).toBeInTheDocument(); + } ); + + test( 'can be dismissed when isDismissible prop is true', () => { + const onRemoveMock = jest.fn(); + const { getByRole } = render( + + This is a success message + + ); + const closeButton = getByRole( 'button' ); + fireEvent.click( closeButton ); + expect( onRemoveMock ).toHaveBeenCalled(); + } ); + + test( 'calls onRemove function when the notice is dismissed', () => { + const onRemoveMock = jest.fn(); + const { getByRole } = render( + + This is an informative message + + ); + const closeButton = getByRole( 'button' ); + fireEvent.click( closeButton ); + expect( onRemoveMock ).toHaveBeenCalled(); + } ); + + test( 'applies the className prop to the notice', () => { + const customClassName = 'my-custom-class'; + const { container } = render( + + This is a success message + + ); + const noticeElement = container.firstChild; + expect( noticeElement ).toHaveClass( customClassName ); + } ); + + test( 'does not throw any errors when all props are provided correctly', () => { + const spyError = jest.spyOn( console, 'error' ); + render( + This is a test message + ); + expect( spyError ).not.toHaveBeenCalled(); // Should not print any error/warning messages + spyError.mockRestore(); // Restore the original mock + } ); +} ); diff --git a/assets/js/base/components/notice-banner/utils.ts b/assets/js/base/components/notice-banner/utils.ts new file mode 100644 index 00000000000..eb061664480 --- /dev/null +++ b/assets/js/base/components/notice-banner/utils.ts @@ -0,0 +1,37 @@ +/** + * External dependencies + */ +import { info, megaphone, check } from '@wordpress/icons'; + +/** + * Get the default politeness level for a given status. This is based on how severe the status is. + */ +export const getDefaultPoliteness = ( status: string ) => { + switch ( status ) { + case 'success': + case 'warning': + case 'info': + case 'default': + return 'polite'; + + case 'error': + default: + return 'assertive'; + } +}; + +/** + * Gets the icon for the notice from the status. Note; we spin the warning status 180 degrees to make it look like an exclamation mark. + */ +export const getStatusIcon = ( status: string ): JSX.Element => { + switch ( status ) { + case 'success': + return check; + case 'warning': + case 'info': + case 'error': + return info; + default: + return megaphone; + } +}; diff --git a/assets/js/base/components/price-slider/index.tsx b/assets/js/base/components/price-slider/index.tsx index 3368882d5a3..5c986fa204c 100644 --- a/assets/js/base/components/price-slider/index.tsx +++ b/assets/js/base/components/price-slider/index.tsx @@ -396,6 +396,22 @@ const PriceSlider = ( {
); + const getInputClassName = ( type: 'min' | 'max' ) => + `wc-block-price-filter__amount wc-block-price-filter__amount--${ type } wc-block-form-text-input wc-block-components-price-slider__amount wc-block-components-price-slider__amount--${ type }`; + + const commonFormattedMonetaryAmountProps = { + currency, + decimalScale: 0, + }; + + const commonFormattedMonetaryAmountInputProps = { + ...commonFormattedMonetaryAmountProps, + displayType: 'input', + allowNegative: false, + disabled: isLoading || ! hasValidConstraints, + onBlur: priceInputOnBlur, + }; + return (
{ ( ! inlineInputAvailable || ! showInputFields ) && slider } @@ -403,14 +419,12 @@ const PriceSlider = ( {
{ ! isUpdating ? ( ) : ( @@ -432,9 +444,8 @@ const PriceSlider = ( { { inlineInputAvailable && slider } { ! isUpdating ? ( ) : ( @@ -465,11 +474,11 @@ const PriceSlider = ( { Number.isFinite( maxPrice ) && (
diff --git a/assets/js/base/components/skeleton/index.tsx b/assets/js/base/components/skeleton/index.tsx new file mode 100644 index 00000000000..6249d108bab --- /dev/null +++ b/assets/js/base/components/skeleton/index.tsx @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import './style.scss'; + +export interface SkeletonProps { + numberOfLines?: number; +} + +export const Skeleton = ( { + numberOfLines = 1, +}: SkeletonProps ): JSX.Element => { + const skeletonLines = Array( numberOfLines ).fill( +
); }; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss b/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss index d8de52ae25e..032d8f5a7cf 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss +++ b/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss @@ -20,20 +20,14 @@ } } -.wc-block-checkout__terms_notice .components-notice__action { - margin-left: 0; -} - -.wc-block-checkout__terms_notice-button { - display: flex; - flex-direction: row; - align-items: center; - - .wc-block-checkout__terms_notice-button__icon { - margin-left: $gap-smallest; +.wc-block-checkout__terms_notice { + margin: 0; + .components-notice__content { + margin: 4px 0; + } + .components-notice__actions { + button { + margin-left: 0; + } } -} - -.wc-block-checkout__terms_notice .components-notice__content { - color: $black; } diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js b/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js index 481ec50a6cf..8c4c1b9aebf 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js +++ b/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, findByRole, queryByText } from '@testing-library/react'; +import { render, queryByText } from '@testing-library/react'; /** * Internal dependencies @@ -12,6 +12,7 @@ const blockSettingsMock = jest.requireMock( '@woocommerce/block-settings' ); jest.mock( '@wordpress/block-editor', () => ( { ...jest.requireActual( '@wordpress/block-editor' ), useBlockProps: jest.fn(), + InspectorControls: jest.fn( ( { children } ) =>
{ children }
), } ) ); jest.mock( '@woocommerce/block-settings', () => ( { @@ -32,7 +33,9 @@ describe( 'Edit', () => { /> ); - expect( await findByRole( container, 'checkbox' ) ).toBeTruthy(); + expect( + queryByText( container, 'I agree to the terms and conditions' ) + ).toBeTruthy(); } ); it( 'Renders a notice if either the terms and conditions or privacy url attribute are unset', async () => { @@ -59,7 +62,7 @@ describe( 'Edit', () => { expect( queryByText( container, - "You don't have any Terms and Conditions and/or Privacy Policy pages set up." + "Link to your store's Terms and Conditions and Privacy Policy pages by creating pages for them." ) ).toBeInTheDocument(); } ); diff --git a/assets/js/blocks/classic-template/archive-product.ts b/assets/js/blocks/classic-template/archive-product.ts index 0e6bdab955a..e9a6b4bb30f 100644 --- a/assets/js/blocks/classic-template/archive-product.ts +++ b/assets/js/blocks/classic-template/archive-product.ts @@ -21,16 +21,8 @@ import { VARIATION_NAME as productsVariationName } from '../product-query/variat import { createArchiveTitleBlock, createRowBlock } from './utils'; import { type InheritedAttributes } from './types'; -const createProductsBlock = ( - inheritedAttributes: InheritedAttributes, - templateInnerBlocks: BlockInstance[] -) => { - const innerBlocks = [ - ...templateInnerBlocks, - ...createBlocksFromInnerBlocksTemplate( productsInnerBlocksTemplate ), - ]; - - return createBlock( +const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) => + createBlock( 'core/query', { ...productsQueryDefaultAttributes, @@ -41,15 +33,14 @@ const createProductsBlock = ( inherit: true, }, }, - innerBlocks + createBlocksFromInnerBlocksTemplate( productsInnerBlocksTemplate ) ); -}; const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes, withTermDescription = false -) => { - const templateInnerBlocks = [ +) => + [ createBlock( 'woocommerce/breadcrumbs', inheritedAttributes ), createArchiveTitleBlock( 'archive-title', inheritedAttributes ), withTermDescription @@ -63,11 +54,9 @@ const getBlockifiedTemplate = ( ], inheritedAttributes ), + createProductsBlock( inheritedAttributes ), ].filter( Boolean ) as BlockInstance[]; - return createProductsBlock( inheritedAttributes, templateInnerBlocks ); -}; - const getBlockifiedTemplateWithTermDescription = ( inheritedAttributes: InheritedAttributes ) => getBlockifiedTemplate( inheritedAttributes, true ); diff --git a/assets/js/blocks/classic-template/product-search-results.ts b/assets/js/blocks/classic-template/product-search-results.ts index 271e77e8ca6..8b1471a2bc1 100644 --- a/assets/js/blocks/classic-template/product-search-results.ts +++ b/assets/js/blocks/classic-template/product-search-results.ts @@ -72,23 +72,13 @@ const extendInnerBlocksWithNoResultsContent = ( ]; }; -const createProductsBlock = ( - inheritedAttributes: InheritedAttributes, - templateInnerBlocks: BlockInstance[] -) => { +const createProductsBlock = ( inheritedAttributes: InheritedAttributes ) => { const productsInnerBlocksWithNoResults = extendInnerBlocksWithNoResultsContent( productsInnerBlocksTemplate, inheritedAttributes ); - const innerBlocks = [ - ...templateInnerBlocks, - ...createBlocksFromInnerBlocksTemplate( - productsInnerBlocksWithNoResults - ), - ]; - return createBlock( 'core/query', { @@ -100,12 +90,12 @@ const createProductsBlock = ( inherit: true, }, }, - innerBlocks + createBlocksFromInnerBlocksTemplate( productsInnerBlocksWithNoResults ) ); }; -const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) => { - const templateInnerBlocks = [ +const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) => + [ createArchiveTitleBlock( 'search-title', inheritedAttributes ), createBlock( 'woocommerce/store-notices', inheritedAttributes ), createRowBlock( @@ -115,11 +105,9 @@ const getBlockifiedTemplate = ( inheritedAttributes: InheritedAttributes ) => { ], inheritedAttributes ), + createProductsBlock( inheritedAttributes ), ].filter( Boolean ) as BlockInstance[]; - return createProductsBlock( inheritedAttributes, templateInnerBlocks ); -}; - const isConversionPossible = () => { // Blockification is possible for the WP version 6.1 and above, // which are the versions the Products block supports. diff --git a/assets/js/blocks/product-query/variations/related-products.tsx b/assets/js/blocks/product-query/variations/related-products.tsx index 2cc212a23d4..0e8f7ff58d3 100644 --- a/assets/js/blocks/product-query/variations/related-products.tsx +++ b/assets/js/blocks/product-query/variations/related-products.tsx @@ -1,11 +1,7 @@ /** * External dependencies */ -import { - InnerBlockTemplate, - registerBlockVariation, - unregisterBlockVariation, -} from '@wordpress/blocks'; +import { BlockAttributes, InnerBlockTemplate } from '@wordpress/blocks'; import { Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { stacks } from '@woocommerce/icons'; @@ -106,35 +102,36 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [ ]; registerBlockSingleProductTemplate( { - registerBlockFn: () => - registerBlockVariation( QUERY_LOOP_ID, { - description: __( - 'Display related products.', - 'woo-gutenberg-products-block' - ), - name: 'Related Products Controls', - title: __( - 'Related Products Controls', - 'woo-gutenberg-products-block' - ), - isActive: ( blockAttributes ) => - blockAttributes.namespace === VARIATION_NAME, - icon: ( - - ), - attributes: BLOCK_ATTRIBUTES, - // Gutenberg doesn't support this type yet, discussion here: - // https://github.com/WordPress/gutenberg/pull/43632 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - allowedControls: [], - innerBlocks: INNER_BLOCKS_TEMPLATE, - scope: [ 'block' ], - } ), - unregisterBlockFn: () => - unregisterBlockVariation( QUERY_LOOP_ID, 'Related Products' ), - blockName: VARIATION_NAME, + blockName: QUERY_LOOP_ID, + blockMetadata: {}, + blockSettings: { + description: __( + 'Display related products.', + 'woo-gutenberg-products-block' + ), + name: 'Related Products Controls', + title: __( + 'Related Products Controls', + 'woo-gutenberg-products-block' + ), + // @ts-expect-error: `isActive` exists on Block Variation configuration + isActive: ( blockAttributes: BlockAttributes ) => + blockAttributes.namespace === VARIATION_NAME, + icon: ( + + ), + attributes: BLOCK_ATTRIBUTES, + // Gutenberg doesn't support this type yet, discussion here: + // https://github.com/WordPress/gutenberg/pull/43632 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + allowedControls: [], + innerBlocks: INNER_BLOCKS_TEMPLATE, + scope: [ 'block' ], + }, + isVariationBlock: true, + variationName: VARIATION_NAME, } ); diff --git a/assets/js/blocks/products/base-utils.js b/assets/js/blocks/products/base-utils.js index fb334f9338e..30e28de693d 100644 --- a/assets/js/blocks/products/base-utils.js +++ b/assets/js/blocks/products/base-utils.js @@ -50,6 +50,11 @@ export const getProductLayoutConfig = ( innerBlocks ) => { block.attributes?.width, } ), } ), + /** + * For product elements, special handing is required if product + * elements are used in the "All Products" block. + */ + isDescendantOfAllProducts: true, }, ]; } ); diff --git a/assets/js/blocks/single-product/block.json b/assets/js/blocks/single-product/block.json new file mode 100644 index 00000000000..f754dd83f29 --- /dev/null +++ b/assets/js/blocks/single-product/block.json @@ -0,0 +1,23 @@ +{ + "name": "woocommerce/single-product", + "version": "1.0.0", + "icon": "info", + "title": "Single Product", + "description": "Display a single product.", + "category": "woocommerce", + "keywords": [ "WooCommerce" ], + "supports": {}, + "attributes": { + "isPreview": { + "type": "boolean", + "default": false + }, + "productId": { + "type": "number" + } + }, + "usesContext": [ "postId", "postType", "queryId" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/assets/js/blocks/single-product/constants.tsx b/assets/js/blocks/single-product/constants.tsx new file mode 100644 index 00000000000..30cc390ce34 --- /dev/null +++ b/assets/js/blocks/single-product/constants.tsx @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { Icon, mediaAndText } from '@wordpress/icons'; +import { getBlockMap } from '@woocommerce/atomic-utils'; +import type { InnerBlockTemplate } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { VARIATION_NAME as PRODUCT_TITLE_VARIATION_NAME } from '../product-query/variations/elements/product-title'; +import { VARIATION_NAME as PRODUCT_SUMMARY_VARIATION_NAME } from '../product-query/variations/elements/product-summary'; + +export const BLOCK_ICON = ( + +); + +export const DEFAULT_INNER_BLOCKS: InnerBlockTemplate[] = [ + [ + 'core/columns', + {}, + [ + [ + 'core/column', + {}, + [ + [ + 'woocommerce/product-image', + { + showSaleBadge: false, + isDescendentOfSingleProductBlock: true, + }, + ], + ], + ], + [ + 'core/column', + {}, + [ + [ + 'core/post-title', + { + headingLevel: 2, + isLink: true, + __woocommerceNamespace: + PRODUCT_TITLE_VARIATION_NAME, + }, + ], + [ + 'woocommerce/product-rating', + { isDescendentOfSingleProductBlock: true }, + ], + [ + 'woocommerce/product-price', + { isDescendentOfSingleProductBlock: true }, + ], + [ + 'core/post-excerpt', + { + __woocommerceNamespace: + PRODUCT_SUMMARY_VARIATION_NAME, + }, + ], + [ 'woocommerce/add-to-cart-form' ], + [ 'woocommerce/product-meta' ], + ], + ], + ], + ], +]; + +export const ALLOWED_INNER_BLOCKS = [ + 'core/columns', + 'core/column', + ...Object.keys( getBlockMap( metadata.name ) ), +]; diff --git a/assets/js/blocks/single-product/edit/editor-block-controls.tsx b/assets/js/blocks/single-product/edit/editor-block-controls.tsx new file mode 100644 index 00000000000..5d195d49e57 --- /dev/null +++ b/assets/js/blocks/single-product/edit/editor-block-controls.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BlockControls } from '@wordpress/block-editor'; +import { ToolbarGroup } from '@wordpress/components'; + +interface EditorBlockControlsProps { + isEditing: boolean; + setIsEditing: ( isEditing: boolean ) => void; +} + +const EditorBlockControls = ( { + isEditing, + setIsEditing, +}: EditorBlockControlsProps ) => { + return ( + + setIsEditing( ! isEditing ), + isActive: isEditing, + }, + ] } + /> + + ); +}; + +export default EditorBlockControls; diff --git a/assets/js/blocks/single-product/edit/editor.scss b/assets/js/blocks/single-product/edit/editor.scss new file mode 100644 index 00000000000..796d2939320 --- /dev/null +++ b/assets/js/blocks/single-product/edit/editor.scss @@ -0,0 +1,14 @@ +.wc-block-editor-single-product__selection { + width: 100%; +} +.wc-block-editor-single-product__reset-layout { + padding: 0; +} +.wc-block-single-product__edit-card { + padding: 16px; + border-top: 1px solid $gray-200; + + .wc-block-single-product__edit-card-title { + margin: 0 0 $gap; + } +} diff --git a/assets/js/blocks/single-product/edit/index.tsx b/assets/js/blocks/single-product/edit/index.tsx new file mode 100644 index 00000000000..7b45dd3bf9c --- /dev/null +++ b/assets/js/blocks/single-product/edit/index.tsx @@ -0,0 +1,137 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { Placeholder, Button, PanelBody } from '@wordpress/components'; +import { withProduct } from '@woocommerce/block-hocs'; +import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; +import EditProductLink from '@woocommerce/editor-components/edit-product-link'; +import { singleProductBlockPreview } from '@woocommerce/resource-previews'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { ProductResponseItem } from '@woocommerce/types'; +import ErrorPlaceholder, { + ErrorObject, +} from '@woocommerce/editor-components/error-placeholder'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import SharedProductControl from './shared-product-control'; +import EditorBlockControls from './editor-block-controls'; +import LayoutEditor from './layout-editor'; +import { BLOCK_ICON } from '../constants'; +import metadata from '../block.json'; +import { Attributes } from '../types'; + +interface EditorProps { + className: string; + attributes: { + productId: number; + isPreview: boolean; + }; + setAttributes: ( attributes: Attributes ) => void; + error: string | ErrorObject; + getProduct: () => void; + product: ProductResponseItem; + isLoading: boolean; + clientId: string; +} + +const Editor = ( { + className, + attributes, + setAttributes, + error, + getProduct, + product, + isLoading, + clientId, +}: EditorProps ) => { + const { productId, isPreview } = attributes; + const [ isEditing, setIsEditing ] = useState( ! productId ); + const blockProps = useBlockProps(); + + if ( isPreview ) { + return singleProductBlockPreview; + } + + if ( error ) { + return ( + + ); + } + + return ( +
+ { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ } + { /* @ts-ignore */ } + + + { isEditing ? ( + + { metadata.description } +
+ + +
+
+ ) : ( +
+ + + + + + + + +
+ ) } +
+
+ ); +}; + +export default withProduct( Editor ); diff --git a/assets/js/blocks/single-product/edit/layout-editor.tsx b/assets/js/blocks/single-product/edit/layout-editor.tsx new file mode 100644 index 00000000000..0f800bb2ad1 --- /dev/null +++ b/assets/js/blocks/single-product/edit/layout-editor.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useCallback } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { + InnerBlockLayoutContextProvider, + ProductDataContextProvider, +} from '@woocommerce/shared-context'; +import { createBlocksFromTemplate } from '@woocommerce/atomic-utils'; +import { PanelBody, Button } from '@wordpress/components'; +import { backup } from '@wordpress/icons'; +import { ProductResponseItem } from '@woocommerce/types'; +import { + InnerBlocks, + InspectorControls, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + BlockContextProvider, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { DEFAULT_INNER_BLOCKS, ALLOWED_INNER_BLOCKS } from '../constants'; +import metadata from '../block.json'; + +interface LayoutEditorProps { + isLoading: boolean; + product: ProductResponseItem; + clientId: string; +} + +const LayoutEditor = ( { + isLoading, + product, + clientId, +}: LayoutEditorProps ) => { + const baseClassName = + '.wc-block-editor-single-product .wc-block-editor-layout'; + const { replaceInnerBlocks } = useDispatch( 'core/block-editor' ); + + const resetInnerBlocks = useCallback( () => { + replaceInnerBlocks( + clientId, + createBlocksFromTemplate( DEFAULT_INNER_BLOCKS ), + false + ); + }, [ clientId, replaceInnerBlocks ] ); + + return ( + + + + + + + +
+ + + +
+
+
+ ); +}; + +export default LayoutEditor; diff --git a/assets/js/blocks/single-product/edit/shared-product-control.tsx b/assets/js/blocks/single-product/edit/shared-product-control.tsx new file mode 100644 index 00000000000..3b2a277fb96 --- /dev/null +++ b/assets/js/blocks/single-product/edit/shared-product-control.tsx @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import ProductControl from '@woocommerce/editor-components/product-control'; + +/** + * Internal dependencies + */ +import { Attributes } from '../types'; + +interface SharedProductControlProps { + attributes: Attributes; + setAttributes: ( attributes: Attributes ) => void; +} + +const SharedProductControl = ( { + attributes, + setAttributes, +}: SharedProductControlProps ) => ( + { + const id = value[ 0 ] ? value[ 0 ].id : 0; + setAttributes( { + productId: id, + } ); + } } + /> +); + +export default SharedProductControl; diff --git a/assets/js/blocks/single-product/index.tsx b/assets/js/blocks/single-product/index.tsx new file mode 100644 index 00000000000..2aa3556604d --- /dev/null +++ b/assets/js/blocks/single-product/index.tsx @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { BLOCK_ICON } from './constants'; +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +// @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. +registerBlockType( metadata, { + icon: BLOCK_ICON, + edit, + save, +} ); diff --git a/assets/js/blocks/single-product/save.tsx b/assets/js/blocks/single-product/save.tsx new file mode 100644 index 00000000000..0feb6d8f950 --- /dev/null +++ b/assets/js/blocks/single-product/save.tsx @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +const Save = () => { + const blockProps = useBlockProps.save(); + + return ( +
+ { /* @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core*/ } + +
+ ); +}; + +export default Save; diff --git a/assets/js/blocks/single-product/types.ts b/assets/js/blocks/single-product/types.ts new file mode 100644 index 00000000000..ea78bc31359 --- /dev/null +++ b/assets/js/blocks/single-product/types.ts @@ -0,0 +1,4 @@ +export interface Attributes { + className?: string; + productId?: number; +} diff --git a/assets/js/data/checkout/selectors.ts b/assets/js/data/checkout/selectors.ts index 7cf19cb6745..9982215995e 100644 --- a/assets/js/data/checkout/selectors.ts +++ b/assets/js/data/checkout/selectors.ts @@ -77,7 +77,7 @@ export const isCalculating = ( state: CheckoutState ) => { }; export const prefersCollection = ( state: CheckoutState ) => { - if ( state.prefersCollection === undefined ) { + if ( typeof state.prefersCollection === 'undefined' ) { const shippingRates = select( cartStoreKey ).getShippingRates(); if ( ! shippingRates || ! shippingRates.length ) { return false; @@ -85,6 +85,7 @@ export const prefersCollection = ( state: CheckoutState ) => { const selectedRate = shippingRates[ 0 ].shipping_rates.find( ( rate ) => rate.selected ); + if ( objectHasProp( selectedRate, 'method_id' ) && isString( selectedRate.method_id ) diff --git a/assets/js/editor-components/default-notice/index.tsx b/assets/js/editor-components/default-notice/index.tsx index 6e862690a3c..c3f85551118 100644 --- a/assets/js/editor-components/default-notice/index.tsx +++ b/assets/js/editor-components/default-notice/index.tsx @@ -1,19 +1,14 @@ /** * External dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { store as editorStore } from '@wordpress/editor'; import triggerFetch from '@wordpress/api-fetch'; import { store as coreStore } from '@wordpress/core-data'; import { Notice, Button } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { CHECKOUT_PAGE_ID, CART_PAGE_ID } from '@woocommerce/block-settings'; -import { - useCallback, - useState, - createInterpolateElement, -} from '@wordpress/element'; -import { getAdminLink } from '@woocommerce/settings'; +import { useCallback, useState } from '@wordpress/element'; /** * Internal dependencies */ @@ -156,36 +151,3 @@ export function DefaultNotice( { block }: { block: string } ) { ); } - -export function LegacyNotice( { block }: { block: string } ) { - return ( - - { createInterpolateElement( - sprintf( - /* translators: %s is the block name. It will be cart or checkout. */ - __( - 'If you would like to use this block as your default %s you must update your page settings in WooCommerce.', - 'woo-gutenberg-products-block' - ), - block - ), - { - a: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - } - ) } - - ); -} diff --git a/assets/js/editor-components/no-payment-methods-notice/editor.scss b/assets/js/editor-components/no-payment-methods-notice/editor.scss new file mode 100644 index 00000000000..a6224675b76 --- /dev/null +++ b/assets/js/editor-components/no-payment-methods-notice/editor.scss @@ -0,0 +1,7 @@ +.wc-blocks-no-payment-methods-notice { + margin: 0; + + .components-notice__content { + margin: 4px 0; + } +} diff --git a/assets/js/editor-components/no-payment-methods-notice/index.tsx b/assets/js/editor-components/no-payment-methods-notice/index.tsx new file mode 100644 index 00000000000..728cffe7269 --- /dev/null +++ b/assets/js/editor-components/no-payment-methods-notice/index.tsx @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Notice, ExternalLink } from '@wordpress/components'; +import { ADMIN_URL } from '@woocommerce/settings'; + +/** + * Internal dependencies + */ +import './editor.scss'; + +export function NoPaymentMethodsNotice() { + const noticeContent = __( + 'Your store does not have any payment methods that support the Checkout block. Once you have configured a compatible payment method it will be displayed here.', + 'woo-gutenberg-products-block' + ); + + return ( + +
+ { noticeContent }{ ' ' } + + { __( + 'Configure Payment Methods', + 'woo-gutenberg-products-block' + ) } + +
+
+ ); +} diff --git a/assets/js/previews/index.js b/assets/js/previews/index.js index 776b0f9a7cf..c64cd463cd8 100644 --- a/assets/js/previews/index.js +++ b/assets/js/previews/index.js @@ -6,3 +6,4 @@ export { previewShippingRates } from './shipping-rates'; export { previewSavedPaymentMethods } from './saved-payment-methods'; export { gridBlockPreview } from './grid-block'; +export { singleProductBlockPreview } from './single-product-block'; diff --git a/assets/js/previews/single-product-block.tsx b/assets/js/previews/single-product-block.tsx new file mode 100644 index 00000000000..e7e86e1d035 --- /dev/null +++ b/assets/js/previews/single-product-block.tsx @@ -0,0 +1,11 @@ +export const singleProductBlockPreview = ( + + + +); diff --git a/assets/js/types/type-defs/index.ts b/assets/js/types/type-defs/index.ts index 3afb7f73155..e6517f5c273 100644 --- a/assets/js/types/type-defs/index.ts +++ b/assets/js/types/type-defs/index.ts @@ -1,20 +1,21 @@ -export * from './api-response'; export * from './api-error-response'; +export * from './api-response'; +export * from './attributes'; export * from './blocks'; -export * from './cart'; export * from './cart-response'; +export * from './cart'; export * from './checkout'; -export * from './currency'; export * from './contexts'; +export * from './currency'; export * from './events'; export * from './hooks'; +export * from './notices'; export * from './objects'; -export * from './payments'; export * from './payment-method-interface'; +export * from './payments'; export * from './product-response'; export * from './shipping'; -export * from './utils'; -export * from './taxes'; -export * from './attributes'; export * from './stock-status'; +export * from './taxes'; +export * from './utils'; export * from './validation'; diff --git a/assets/js/types/type-defs/notices.ts b/assets/js/types/type-defs/notices.ts new file mode 100644 index 00000000000..921da712db6 --- /dev/null +++ b/assets/js/types/type-defs/notices.ts @@ -0,0 +1,12 @@ +/** + * External dependencies + */ +import type { Notice } from '@wordpress/notices'; + +export interface NoticeType extends Partial< Omit< Notice, 'status' > > { + id: string; + content: string; + status: 'success' | 'error' | 'info' | 'warning' | 'default'; + isDismissible: boolean; + context?: string | undefined; +} diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 4f1900fcbcd..3b03f77d997 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -64,6 +64,9 @@ const blocks = { 'reviews-by-product': { customDir: 'reviews/reviews-by-product', }, + 'single-product': { + isExperimental: true, + }, 'stock-filter': {}, }; diff --git a/composer.json b/composer.json index 0a13a9a28cc..b2be18e3cd8 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "WooCommerce blocks for the Gutenberg editor.", "homepage": "https://woocommerce.com/", "type": "wordpress-plugin", - "version": "9.9.0-dev", + "version": "10.0.0-dev", "keywords": [ "gutenberg", "woocommerce", diff --git a/docs/contributors/contributing/getting-started.md b/docs/contributors/contributing/getting-started.md index f7ae043eb99..19e0fc81f78 100644 --- a/docs/contributors/contributing/getting-started.md +++ b/docs/contributors/contributing/getting-started.md @@ -2,18 +2,18 @@ ## Table of contents -- [Cloning the Git Repository](#cloning-the-git-repository) -- [Configuring your WordPress site](#configuring-your-wordpress-site) -- [Installing dependencies](#installing-dependencies) -- [Building the plugin files](#building-the-plugin-files) -- [Create a plugin package in ZIP format](#create-a-plugin-package-in-zip-format) -- [Linting](#linting) -- [Running the Blocks plugin](#running-the-blocks-plugin) -- [Developer Tools (Visual Studio Code)](#developer-tools-visual-studio-code) - - [EditorConfig](#editorconfig) - - [ESLint](#eslint) - - [Prettier](#prettier) -- [Testing](#testing) +- [Cloning the Git Repository](#cloning-the-git-repository) +- [Configuring your WordPress site](#configuring-your-wordpress-site) +- [Installing dependencies](#installing-dependencies) +- [Building the plugin files](#building-the-plugin-files) +- [Create a plugin package in ZIP format](#create-a-plugin-package-in-zip-format) +- [Linting](#linting) +- [Running the Blocks plugin](#running-the-blocks-plugin) +- [Developer Tools (Visual Studio Code)](#developer-tools-visual-studio-code) + - [EditorConfig](#editorconfig) + - [ESLint](#eslint) + - [Prettier](#prettier) +- [Testing](#testing) Before you can begin contributing to the Blocks plugin there are several steps and tools required to setup your local development environment. @@ -24,7 +24,7 @@ Before you can start modifying files you'll want to clone this repository locall To do so from the command line, ensure you have [`git`](https://git-scm.com) installed on your machine, and run the clone command: ```sh -git clone https://github.com/woocommerce/woocommerce-gutenberg-products-block.git +git clone https://github.com/woocommerce/woocommerce-blocks.git ``` ## Configuring your WordPress site @@ -48,15 +48,17 @@ define( 'SCRIPT_DEBUG', true ); To install dependencies, you will need the following tools installed on your machine: -- [`npm` and `node.js`](https://nodejs.org) -- [`composer`](https://getcomposer.org) +- `node` and `npm` via [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node or NPM through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](../../../.nvmrc) which helps ensure you are using the correct version of Node. +- [PHP](https://www.php.net/manual/en/install.php): WooCommerce Blocks requires PHP. It is also needed to run Composer and various project build scripts. +- [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins. -See [`package.json` `engines`](../../../package.json) for details of required versions. +See [`package.json` `engines`](../../../package.json) and [`readme.txt`](../../../readme.txt#L6) for details on required versions. + +Once you the above setup, install the dependencies from the command line: -Once you have `node` and `composer` setup, install the dependencies from the command line: - -- Change directory to your repo folder, e.g. `$ cd woocommerce-gutenberg-products-block`. -- Install javascript and php dependencies - `$ npm install && composer install`. +- Change directory to your repo folder, e.g. `$ cd woocommerce-blocks`. +- Ensure the correct version of Node and NPM are installed - `$ nvm use` +- Install JavaScript and PHP dependencies - `$ npm install && composer install`. ## Building the plugin files diff --git a/docs/internal-developers/testing/releases/990.md b/docs/internal-developers/testing/releases/990.md new file mode 100644 index 00000000000..02880ef5923 --- /dev/null +++ b/docs/internal-developers/testing/releases/990.md @@ -0,0 +1,303 @@ +# Testing notes and ZIP for release 9.9.0 + +Zip file for testing: [woocommerce-gutenberg-products-block.zip](https://github.com/woocommerce/woocommerce-blocks/files/11113569/woocommerce-gutenberg-products-block.zip) + +## WooCommerce Core + + +### Move Related Products's notice component to the Inspector Control section. ([8843](https://github.com/woocommerce/woocommerce-blocks/pull/8843)) + +1. Go to Appearance > Themes and activate a Blockfied theme, such as: Twenty-twenty Three; +2. After the theme is activated, go to Appearance > Editor (Beta); +3. Inside the Design section, click on Templates; +4. Select the Single Product template; +5. On the top left of the page, click on the Edit button; +6. Using the Block Inserter, type: Related Products, and add the block to the Site Editor; +7. Click on the Save button; +8. Check that the Skeleton component appears above the Add To Cart button. + +| Before | After | +| ------ | ----- | +| image | image | + +### Product SKU Block: Don't render the prefix when the SKU isn't defined. ([8837](https://github.com/woocommerce/woocommerce-blocks/pull/8837)) + +1. Create a post/page. +2. Add the `Products` block. +3. Inside the `Products` block, add the Product SKU block. +4. Save the post/page. +5. Edit a Product that will be visible in `Products` block. +6. Scroll down until you see the “Inventory” section. In that section, it is visible a field labeled “SKU.” Remove the SKU in that field, and then click on the “Update” button at the bottom of the page. +7. On the front end, visit the saved post/page and check the `Products` block. +8. Ensure that the edited product doesn't have the SKU prefix visible. + +| Before | After | +|--------|--------| +|![image](https://user-images.githubusercontent.com/4463174/227186842-d7f4a673-d943-4f40-a897-901139e92cd9.png)|![image](https://user-images.githubusercontent.com/4463174/227193368-f7f0db7e-115d-4590-a9e3-b16af1b2538e.png)| + +### Mini-cart: Add the option to change the style between 'Outline' and 'Fill' in all the buttons. ([8835](https://github.com/woocommerce/woocommerce-blocks/pull/8835)) + +1. Add the `Mini cart` block to the header template of your site. +2. Go to the `Site Editor` and edit the `Mini cart` template part. +3. Open the `List view` and select the `Empty Mini Cart view`. +4. Click on the `Start shopping` button, check that you can change the button style between `Default` and `Outline` in the sidebar settings and change it from the default and save. +6. Go to the frontend and check the button has the style you just picked on the Site editor. +7. Repeat the same steps for the `Filled Mini Cart view` and the `View my cart` and the `Go to checkout` buttons. + +### Product SKU: Make the block focusable in editor. ([8804](https://github.com/woocommerce/woocommerce-blocks/pull/8804)) + +1. Create a new post +2. Add Products block +3. Add Product SKU block to Products +4. Hover and click on the product +5. **Expected:** Product SKU can be focused on and contextual menu is displayed. Block can be moved up and down to switch places with other blocks. + +| Before | After | +| ------ | ----- | +| image | image | + + +### Add Fill & Outline styles, width settings & new typography controls for Product(Add to cart) button block. ([8781](https://github.com/woocommerce/woocommerce-blocks/pull/8781)) + + +- Add `Products block` to a page/post. +- Select `Add to cart` inner block. +- In the sidebar, confirm newly added controls work as expected: + - Styles: Default & Outline ( Feature plugin ) + - Width Settings ( Feature Plugin + WooCommerce Core ) + - Typography controls ( Feature plugin ) +- Confirm `Add to cart` works for cross-sells as well. + +image + + +### Add style to the `Mini Cart` buttons. ([8776](https://github.com/woocommerce/woocommerce-blocks/pull/8776)) + +1. Add the `Mini cart` block to the header template of your site. +2. Go to the `Site Editor` and edit the `Mini cart` template part. +3. Open the `List view` and select the `Filled Mini Cart view`. +4. Click on the `View my cart` button and check that you can change the background and text colors of the button. +5. Click on the `Go to checkout` button and check that you can change the background and text colors of the button. +7. Save, go to the frontend, and check the buttons have the colors you just picked on the Site editor. + +### Add the ability to change the background and text colors of the Mini Cart block "Start shopping" button. ([8766](https://github.com/woocommerce/woocommerce-blocks/pull/8766)) + +1. Add the `Mini cart` block to the header template of your site. +2. Go to the `Site Editor` and edit the `Mini cart` template part. +3. Open the `List view` and select the `Empty Mini Cart view`. +4. Click on the `Start shopping` button and check that you can change the background and text colors of the button. +5. Change both of them and save. +6. Go to the frontend and check the button has the colors you just picked on the Site editor. + +### Mini-cart: Add setting to not render the block on the cart & checkout pages. ([8700](https://github.com/woocommerce/woocommerce-blocks/pull/8700)) + +#### Site Editor + +1. Go to the Site Editor > Template parts and edit the header template to add the `Mini Cart` block. Save. +2. Edit the block and make sure you see the new setting: + +Screenshot 2023-03-13 at 16 27 26 + +3. Make sure the default option is `Hide`. +4. In the store, go to the Cart page and make sure the `Mini Cart` is rendered but invisible. Repeat but for the Checkout page. +5. Go back to the Site Editor, change the `Mini Cart` setting to `Remove`, and save. +6. In the store, go to the Cart page and make sure the `Mini Cart` markup is not rendered at all. Repeat but for the Checkout page. + + +#### Post/page + +1. Create a new post or page. +2. Insert the `Mini Cart`. +3. Make sure the new `Mini Cart in cart and checkout pages` setting does not appear. + + +### Ensure shipping rates do not show in the Checkout block if the "Hide shipping costs until an address is entered option is selected". ([8682](https://github.com/woocommerce/woocommerce-blocks/pull/8682)) + + +1. Go to `WooCommerce -> Settings -> Shipping -> Local Pickup`, enable Local Pickup and add a location. +2. Go to the Checkout block in the Page editor, click on the Shipping Options block - in the block sidebar, enable the `Hide shipping costs until an address is entered` option. +3. In WooCommerce -> Settings -> General change `Default customer location` to `No location by default`. +4. In an incognito window, add an item to your cart and go to the Checkout block. +5. Ensure no shipping rates are shown, and ensure the "Shipping" button for method selection says `calculated with an address` +6. Enter an address (one that you have rates set up for) - as you fill in the address, be sure the rates don't populate until it's completely filled in. (address 1, city, state, country, postcode). +7. Ensure you can change rates etc. +8. Disable the `Hide shipping costs until an address is entered` in the Page editor and ensure the rates show as usual as soon as the country/state are entered and the rest of the address is empty. (note you should enter a country that you have rates for!). + + +### Move option to hide shipping costs until an address is entered to the Checkout block. ([8680](https://github.com/woocommerce/woocommerce-blocks/pull/8680)) + + +1. Go to WooCommerce -> Settings -> Shipping -> Local Pickup, enable Local Pickup and add a location. +2. Go to the Checkout block in the Page editor. Select the `Shipping Method` block +3. See the `Hide shipping costs until an address is entered` option in the block sidebar. +4. Toggle it and ensure it works. Remember the value you toggled it to. +5. Select the `Shipping options` block, see the same option. Ensure the value is the same as what it was in step 4. +6. If the option is `true` then you should see the text `Shipping options will be displayed here after entering your full shipping address.` instead of shipping options. +7. Toggle the option and ensure the inner block changes to either shipping options or the text. +8. Go back to the `Shipping Method` block and toggle the option there. View the `Shipping options` block while toggling and ensure it changes. + + +| Before | After | +| ------ | ----- | +| image | image | + + +### Remove certain Shipping settings from WooCommerce -> Settings -> Shipping -> Shipping Options when using the Cart or Checkout blocks, these have been moved to setting on the blocks. ([8679](https://github.com/woocommerce/woocommerce-blocks/pull/8679)) + +1. Go to `WooCommerce -> Settings -> Advanced` - set the Cart and Checkout pages to pages containing the **shortcode** cart/checkout experience. Save. +2. Go to `WooCommerce -> Settings -> Shipping -> Shipping Options` - observe the two options, `Hide shipping costs until an address is entered` and `Enable the shipping calculator on the cart page`. +3. Go to `WooCommerce -> Settings -> Advanced` - set the **Cart** page to one containing the **Cart Block**. Save. +4. Go to `WooCommerce -> Settings -> Shipping -> Shipping Options` - observe that where the two options were in step 2, only, `Hide shipping costs until an address is entered` remains. Ensure it displays correctly and looks OK. +5. Go to `WooCommerce -> Settings -> Advanced` - set the **Checkout** page to one containing the **Checkout Block**. Save. +6. Go to `WooCommerce -> Settings -> Shipping -> Shipping Options` - observe that where the two options were in step 2 nothing appears. +7. Go to `WooCommerce -> Settings -> Advanced` - set the **Cart** page to one containing the **Shortcode cart**. Save. +8. Go to `WooCommerce -> Settings -> Shipping -> Shipping Options` - observe that where the two options were in step 2 only `Enable the shipping calculator on the cart page` appears. Ensure it displays correctly. + +Expected results + +| | Shortcode Cart | Block Cart | +|-|----------------|------------| +| **Shortcode Checkout** | image | image | +| **Block Checkout** | image | image | + +### Add spacing between Mini Cart title and products list when scrolled. ([8676](https://github.com/woocommerce/woocommerce-blocks/pull/8676)) + + +1. With a block theme, add the Mini Cart block to the header of your site. +2. Add many products to your cart. +3. Click on the Mini Cart button to open the drawer. +4. Scroll down the list of products in the Mini Cart drawer and verify there is some space between the title and product list. +5. Go to Appearance > Editor > Template parts and edit the Mini Cart template part. +6. Change the background color to something different. +7. Repeat steps 3 and 4 and verify the space between the Mini Cart title and the products list honors that color. + +Before | After | After (with custom background color) +--- | --- | --- +![imatge](https://user-images.githubusercontent.com/3616980/223775624-f4b1b78e-d6bd-4698-a2a8-096083a0e8ba.png) | ![imatge](https://user-images.githubusercontent.com/3616980/223775552-b3255fbe-b4de-435a-81e4-913b2b7e92e7.png) | ![imatge](https://user-images.githubusercontent.com/3616980/223775429-e55069e8-5007-44bd-a4d3-1dc43a2fdfef.png) + + +### Add new ExperimentalOrderLocalPickupPackages Slot/Fill. ([8636](https://github.com/woocommerce/woocommerce-blocks/pull/8636)) + + +1. Go to WooCommerce -> Settings -> Shipping -> Local Pickup and activate Local Pickup. Ensure you have added a couple of locations. +2. Add an item to your cart and go to the Checkout block. +3. Select Local Pickup and ensure the options you set up in step 1 are visible. + + +### Add client side postcode validation. ([8503](https://github.com/woocommerce/woocommerce-blocks/pull/8503)) + +1. Add a product to the Cart and go to the Checkout block page. +2. Select United Kingdom (UK) as country. +3. Verify that the postcode `AA9A 9AA` passes the validation. +4. Verify that the postcode `9999 999` fails the validation. +5. Verify that it's not possible to have spaces before the postcode, .e.g. ` AA9A 9AA`. +6. Verify that lowercase letters, e.g. `aa9A 9aa`, are automatically converted to uppercase letters, e.g. `AA9A 9AA`. + + +### Display pickup location details in order confirmations. ([8727](https://github.com/woocommerce/woocommerce-blocks/pull/8727)) + +1. Place an order using the Checkout block, selecting Local Pickup as your shipping method. +2. Check the order confirmation page shows pickup details. +3. Confirm the order confirmation page hides the "shipping address". +4. Check the order email confirmation shows the same pickup details. + +![Screenshot 2023-03-14 at 12 09 47](https://user-images.githubusercontent.com/90977/224999438-6a914f5d-6196-4d93-aa50-f4af6c018d61.png) + +### Local Pickup: Merge country and state into same field in location modal. ([8408](https://github.com/woocommerce/woocommerce-blocks/pull/8408)) + + +1. `Enable local pickup` in `WooCommerce -> Settings -> Shipping -> Local pickup -> General`. +2. Under `Pickup locations`, click on `Add pickup location`. The `Pickup location` modal should open +3. Choose a country with a state. Ensure it's displayed in one field +4. Fill in the other fields of the modal form, click on `done`, then save changes +5. Go to your store, add a product to the cart, then go to the Checkout Block page +6. Under the `Shipping method` section, select `Local Pickup` +7. Ensure the correct country and state you previously selected are correctly showing under `Pickup options` +8. Go back to the local pickup settings from step 1. Follow the same steps from 2 to 7, but in step 3, choose a country without a state (e.g., Cameroon) and ensure that the `State` text field is displayed. Fill in the form and continue with the rest of the instructions. + +| Before | After | +| ------ | ----- | +| image | image | + + +### Enable users to migrate to the blockified Single Product template. ([8324](https://github.com/woocommerce/woocommerce-blocks/pull/8324)) + +1. Enter the `Single Product` template. +2. Check that the placeholder description says "This block serves as a placeholder for your WooCommerce Single Product Block. We recommend upgrading to the Single Products block for more features to edit your products visually. Don't worry, you can always revert back.". +3. Click the `Upgrade to Blockified Single Product Template` button. +4. See new templates work in the Site Editor and on the front end. + +| Before | After | +| ------ | ----- | +| Screenshot 2023-01-30 at 12 41 58 |
- - - + + + + + + + +
+ ``` -In the `wp:woocommerce/product-search` substitute the URL used for the `action` attribute to your site URL or the block will not embedd correctly. - -### 2. Create a page with the All Products Block, and some Filter Blocks, setup to test that functionality in isolation. Using the columns block here too is a good idea to keep things organized +### 2. Create a page with the Products block, and filter blocks, setup to test that functionality in isolation. Using the columns block here too is a good idea to keep things organized
-You can copy and paste the following code into a new page to add all the blocks (click): +You can copy and paste (Ctrl+Shift+V) the following code into a new page to add all the blocks (click): ```html - -
- -
- -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- -
- - - -
- -
- -
- -
+ +
+
+
+

Filter by price

+ + + +
+
+ + + +
+

Filter by attribute

+ + + +
+
+ + + +
+

Filter by stock status

+ + + +
+
+ + + +
+

Filter by rating

+ + + +
+
+ + + +
+

Active filters

+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + +

+ +
+
+
``` @@ -263,9 +217,9 @@ In the `wp:woocommerce/product-search` substitute the URL used for the `action` - [ ] Is the Browser error console free from errors/notices/warnings? - [ ] Test inserting various blocks into the editor - [ ] This can be verified by copying and pasting the code examples above. However, please do also test manually inserting the next three blocks as representative examples for related blocks. - - [ ] All Products Blocks (this is powered by the Store API) - - [ ] Featured Product (this is powered by the REST API) - - [ ] On Sale Products (this is SSR) + - [ ] All Products Blocks (this is powered by the Store API) + - [ ] Featured Product (this is powered by the REST API) + - [ ] On Sale Products (this is SSR) - [ ] Is the Browser error console free from errors/notices/warnings after inserting them? - [ ] Do they persist and continue to display correctly after save/refresh? diff --git a/package-lock.json b/package-lock.json index 8fa5dc5735d..216cc89054c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@woocommerce/block-library", - "version": "9.9.0-dev", + "version": "10.0.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@woocommerce/block-library", - "version": "9.9.0-dev", + "version": "10.0.0-dev", "hasInstallScript": true, "license": "GPL-3.0+", "dependencies": { @@ -38,6 +38,7 @@ "postcode-validator": "3.7.0", "preact": "^10.11.3", "react-number-format": "4.9.3", + "react-transition-group": "^4.4.5", "reakit": "1.3.11", "snakecase-keys": "5.4.2", "trim-html": "0.1.9", @@ -79,6 +80,7 @@ "@types/puppeteer": "5.4.6", "@types/react": "18.0.29", "@types/react-dom": "18.0.10", + "@types/react-transition-group": "^4.4.5", "@types/wordpress__block-editor": "6.0.6", "@types/wordpress__blocks": "11.0.9", "@types/wordpress__components": "^23.0.0", @@ -104,7 +106,7 @@ "@wordpress/data-controls": "2.2.7", "@wordpress/dependency-extraction-webpack-plugin": "3.2.1", "@wordpress/dom": "3.27.0", - "@wordpress/e2e-test-utils": "9.2.0", + "@wordpress/e2e-test-utils": "10.1.0", "@wordpress/e2e-tests": "4.6.0", "@wordpress/element": "4.20.0", "@wordpress/env": "^4.9.0", @@ -11507,6 +11509,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "dev": true, @@ -14052,15 +14063,15 @@ } }, "node_modules/@wordpress/e2e-test-utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-9.2.0.tgz", - "integrity": "sha512-kfnIwgoo4fd1+h85btWFyoMcPBqYA4szwWI3GmrBny8ybyFljRed1XVleZ/oD6MaFgid4uB6V4+OKTlsw5mg/Q==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-10.1.0.tgz", + "integrity": "sha512-FybFtoN7qq+3ne+Sy8OThUBvBtm3Qd5Rh8BQ0hXdE3izIclwqPVePlDOhyz9AHvOajmgLn4v/YXs3D1uOOhVKA==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "^6.22.0", - "@wordpress/keycodes": "^3.25.0", - "@wordpress/url": "^3.26.0", + "@wordpress/api-fetch": "^6.27.0", + "@wordpress/keycodes": "^3.30.0", + "@wordpress/url": "^3.31.0", "change-case": "^4.1.2", "form-data": "^4.0.0", "node-fetch": "^2.6.0" @@ -14069,32 +14080,32 @@ "node": ">=14" }, "peerDependencies": { - "jest": ">=27", + "jest": ">=29", "puppeteer-core": ">=11" } }, "node_modules/@wordpress/e2e-test-utils/node_modules/@wordpress/api-fetch": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.22.0.tgz", - "integrity": "sha512-IO7Shv1Qz93bo/Rq20beAV+p1qSOCs4uUi98rzhhih7U0SF88Jo69mlNmQbpALWcG040a2DRQR8E18Mj7JwViQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.27.0.tgz", + "integrity": "sha512-5DAOOhvA4rmAGIoQX8gbut73Ls0nD5vKo4ImhaeMt+5o7JO7Mn+FU1t7GFy2MwzdBEu/toh/XuCHqF6F3HaooQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.25.0", - "@wordpress/url": "^3.26.0" + "@wordpress/i18n": "^4.30.0", + "@wordpress/url": "^3.31.0" }, "engines": { "node": ">=12" } }, "node_modules/@wordpress/e2e-test-utils/node_modules/@wordpress/i18n": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.25.0.tgz", - "integrity": "sha512-cKU5Ox1DKa3WShRu+QrCU+QzNvyoQhrNtS6kcvw17DfMBjPe7AsYjd7ZBb7Io327jP97Oqh5BtaYdUq/4S1cIw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.30.0.tgz", + "integrity": "sha512-vIntwrTBSU2MXOBlpyFntPgimHP+RW+k7/Y00BMPL+xoxPr7J7sXX/GNoYlH1BNsAo7XOi5AY5FrUnQ7ZIYdtQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.25.0", + "@wordpress/hooks": "^3.30.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -14108,9 +14119,9 @@ } }, "node_modules/@wordpress/e2e-test-utils/node_modules/@wordpress/url": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.26.0.tgz", - "integrity": "sha512-oqIYWqUo1sr1qU4jxbRhzusSqMClSHn4bNtlR835VcqZoBnTM7/RwfrmTo6aCWDcSAd7LQVV1vykTjJsAYdxsA==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.31.0.tgz", + "integrity": "sha512-3sxbDKU8OQTeGat3Roef9dN/NR0Rb6ld9KN0618Ec+FHU05dI+7nolA0jfOAJka+Vvf7gfP0WQ7cJAZdlwfNuw==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", @@ -16254,9 +16265,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.27.0.tgz", - "integrity": "sha512-izhRvOJzc/VFsu59KC+et1/35GL0Op7I60RZj2lkTnEz1vGvtClY3okCbOtGN0Adc8ewbTf4kB6qgKMsLtW0Dg==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.30.0.tgz", + "integrity": "sha512-cvM5OWMQx0D2+wxvsTCI68LXy4cHz1Db97LliiQ+KprSBmYRG5Uy2PqtPv1sMlK8IOypKOlzmG5H5V1CwBN44A==", "dependencies": { "@babel/runtime": "^7.16.0" }, @@ -16412,26 +16423,25 @@ "license": "MIT" }, "node_modules/@wordpress/keycodes": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.25.0.tgz", - "integrity": "sha512-y55wL9bj/XrW7Uyvg6kyQeVjPQOezG7HTI+L3nzI12waL50eGhqM1DSOv6PhMrcoHG4CN5Lg8bXNUF6TrAntfA==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.30.0.tgz", + "integrity": "sha512-rDZYk/t3a/WtOi8SrfVrHP63mT5NXw13kNf3+VL/jk+hcacb6TXImFEwH0F5nLHVYpvv0fPSoFUDo5bYqwbHZQ==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.25.0", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "@wordpress/i18n": "^4.30.0", + "change-case": "^4.1.2" }, "engines": { "node": ">=12" } }, "node_modules/@wordpress/keycodes/node_modules/@wordpress/i18n": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.25.0.tgz", - "integrity": "sha512-cKU5Ox1DKa3WShRu+QrCU+QzNvyoQhrNtS6kcvw17DfMBjPe7AsYjd7ZBb7Io327jP97Oqh5BtaYdUq/4S1cIw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.30.0.tgz", + "integrity": "sha512-vIntwrTBSU2MXOBlpyFntPgimHP+RW+k7/Y00BMPL+xoxPr7J7sXX/GNoYlH1BNsAo7XOi5AY5FrUnQ7ZIYdtQ==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.25.0", + "@wordpress/hooks": "^3.30.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -20406,15 +20416,6 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "dev": true, @@ -25031,6 +25032,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-scroll-into-view": { "version": "1.2.1", "license": "MIT" @@ -27150,12 +27160,6 @@ "ramda": "^0.28.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "dev": true, @@ -28024,17 +28028,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.2", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "license": "MIT" @@ -38336,11 +38329,6 @@ "dev": true, "license": "ISC" }, - "node_modules/nan": { - "version": "2.16.0", - "license": "MIT", - "optional": true - }, "node_modules/nanoid": { "version": "3.3.4", "dev": true, @@ -38611,16 +38599,6 @@ "inherits": "2.0.3" } }, - "node_modules/node-pty": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.9.0.tgz", - "integrity": "sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "nan": "^2.14.0" - } - }, "node_modules/node-releases": { "version": "2.0.4", "license": "MIT" @@ -43115,6 +43093,21 @@ "react": "17.0.2" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-use-gesture": { "version": "9.1.3", "license": "MIT", @@ -48871,23 +48864,6 @@ "node": ">=0.10.0" } }, - "node_modules/watchpack-chokidar2/node_modules/fsevents": { - "version": "1.2.13", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, "node_modules/watchpack-chokidar2/node_modules/glob-parent": { "version": "3.1.0", "dev": true, @@ -58790,6 +58766,15 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.0", "dev": true, @@ -60752,39 +60737,39 @@ } }, "@wordpress/e2e-test-utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-9.2.0.tgz", - "integrity": "sha512-kfnIwgoo4fd1+h85btWFyoMcPBqYA4szwWI3GmrBny8ybyFljRed1XVleZ/oD6MaFgid4uB6V4+OKTlsw5mg/Q==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-10.1.0.tgz", + "integrity": "sha512-FybFtoN7qq+3ne+Sy8OThUBvBtm3Qd5Rh8BQ0hXdE3izIclwqPVePlDOhyz9AHvOajmgLn4v/YXs3D1uOOhVKA==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "^6.22.0", - "@wordpress/keycodes": "^3.25.0", - "@wordpress/url": "^3.26.0", + "@wordpress/api-fetch": "^6.27.0", + "@wordpress/keycodes": "^3.30.0", + "@wordpress/url": "^3.31.0", "change-case": "^4.1.2", "form-data": "^4.0.0", "node-fetch": "^2.6.0" }, "dependencies": { "@wordpress/api-fetch": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.22.0.tgz", - "integrity": "sha512-IO7Shv1Qz93bo/Rq20beAV+p1qSOCs4uUi98rzhhih7U0SF88Jo69mlNmQbpALWcG040a2DRQR8E18Mj7JwViQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.27.0.tgz", + "integrity": "sha512-5DAOOhvA4rmAGIoQX8gbut73Ls0nD5vKo4ImhaeMt+5o7JO7Mn+FU1t7GFy2MwzdBEu/toh/XuCHqF6F3HaooQ==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.25.0", - "@wordpress/url": "^3.26.0" + "@wordpress/i18n": "^4.30.0", + "@wordpress/url": "^3.31.0" } }, "@wordpress/i18n": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.25.0.tgz", - "integrity": "sha512-cKU5Ox1DKa3WShRu+QrCU+QzNvyoQhrNtS6kcvw17DfMBjPe7AsYjd7ZBb7Io327jP97Oqh5BtaYdUq/4S1cIw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.30.0.tgz", + "integrity": "sha512-vIntwrTBSU2MXOBlpyFntPgimHP+RW+k7/Y00BMPL+xoxPr7J7sXX/GNoYlH1BNsAo7XOi5AY5FrUnQ7ZIYdtQ==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.25.0", + "@wordpress/hooks": "^3.30.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -60792,9 +60777,9 @@ } }, "@wordpress/url": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.26.0.tgz", - "integrity": "sha512-oqIYWqUo1sr1qU4jxbRhzusSqMClSHn4bNtlR835VcqZoBnTM7/RwfrmTo6aCWDcSAd7LQVV1vykTjJsAYdxsA==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.31.0.tgz", + "integrity": "sha512-3sxbDKU8OQTeGat3Roef9dN/NR0Rb6ld9KN0618Ec+FHU05dI+7nolA0jfOAJka+Vvf7gfP0WQ7cJAZdlwfNuw==", "dev": true, "requires": { "@babel/runtime": "^7.16.0", @@ -62265,9 +62250,9 @@ } }, "@wordpress/hooks": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.27.0.tgz", - "integrity": "sha512-izhRvOJzc/VFsu59KC+et1/35GL0Op7I60RZj2lkTnEz1vGvtClY3okCbOtGN0Adc8ewbTf4kB6qgKMsLtW0Dg==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.30.0.tgz", + "integrity": "sha512-cvM5OWMQx0D2+wxvsTCI68LXy4cHz1Db97LliiQ+KprSBmYRG5Uy2PqtPv1sMlK8IOypKOlzmG5H5V1CwBN44A==", "requires": { "@babel/runtime": "^7.16.0" } @@ -62363,23 +62348,22 @@ } }, "@wordpress/keycodes": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.25.0.tgz", - "integrity": "sha512-y55wL9bj/XrW7Uyvg6kyQeVjPQOezG7HTI+L3nzI12waL50eGhqM1DSOv6PhMrcoHG4CN5Lg8bXNUF6TrAntfA==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.30.0.tgz", + "integrity": "sha512-rDZYk/t3a/WtOi8SrfVrHP63mT5NXw13kNf3+VL/jk+hcacb6TXImFEwH0F5nLHVYpvv0fPSoFUDo5bYqwbHZQ==", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.25.0", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "@wordpress/i18n": "^4.30.0", + "change-case": "^4.1.2" }, "dependencies": { "@wordpress/i18n": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.25.0.tgz", - "integrity": "sha512-cKU5Ox1DKa3WShRu+QrCU+QzNvyoQhrNtS6kcvw17DfMBjPe7AsYjd7ZBb7Io327jP97Oqh5BtaYdUq/4S1cIw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.30.0.tgz", + "integrity": "sha512-vIntwrTBSU2MXOBlpyFntPgimHP+RW+k7/Y00BMPL+xoxPr7J7sXX/GNoYlH1BNsAo7XOi5AY5FrUnQ7ZIYdtQ==", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.25.0", + "@wordpress/hooks": "^3.30.0", "gettext-parser": "^1.3.1", "memize": "^1.1.0", "sprintf-js": "^1.1.1", @@ -65086,14 +65070,6 @@ "version": "2.2.0", "devOptional": true }, - "bindings": { - "version": "1.5.0", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bl": { "version": "4.1.0", "dev": true, @@ -68246,6 +68222,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-scroll-into-view": { "version": "1.2.1" }, @@ -69697,11 +69682,6 @@ "ramda": "^0.28.0" } }, - "file-uri-to-path": { - "version": "1.0.0", - "dev": true, - "optional": true - }, "filename-reserved-regex": { "version": "2.0.0", "dev": true @@ -70274,10 +70254,6 @@ "version": "1.0.0", "devOptional": true }, - "fsevents": { - "version": "2.3.2", - "optional": true - }, "function-bind": { "version": "1.1.1" }, @@ -77407,10 +77383,6 @@ "version": "0.0.8", "dev": true }, - "nan": { - "version": "2.16.0", - "optional": true - }, "nanoid": { "version": "3.3.4", "dev": true @@ -77613,15 +77585,6 @@ } } }, - "node-pty": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.9.0.tgz", - "integrity": "sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g==", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, "node-releases": { "version": "2.0.4" }, @@ -80613,6 +80576,17 @@ "scheduler": "^0.20.2" } }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "react-use-gesture": { "version": "9.1.3", "requires": {} @@ -84541,15 +84515,6 @@ } } }, - "fsevents": { - "version": "1.2.13", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, "glob-parent": { "version": "3.1.0", "dev": true, diff --git a/package.json b/package.json index 18495ae724b..4de8b5eb733 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@woocommerce/block-library", "title": "WooCommerce Blocks", "author": "Automattic", - "version": "9.9.0-dev", + "version": "10.0.0-dev", "description": "WooCommerce blocks for the Gutenberg editor.", "homepage": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/", "keywords": [ @@ -123,6 +123,7 @@ "@types/puppeteer": "5.4.6", "@types/react": "18.0.29", "@types/react-dom": "18.0.10", + "@types/react-transition-group": "^4.4.5", "@types/wordpress__block-editor": "6.0.6", "@types/wordpress__blocks": "11.0.9", "@types/wordpress__components": "^23.0.0", @@ -148,7 +149,7 @@ "@wordpress/data-controls": "2.2.7", "@wordpress/dependency-extraction-webpack-plugin": "3.2.1", "@wordpress/dom": "3.27.0", - "@wordpress/e2e-test-utils": "9.2.0", + "@wordpress/e2e-test-utils": "10.1.0", "@wordpress/e2e-tests": "4.6.0", "@wordpress/element": "4.20.0", "@wordpress/env": "^4.9.0", @@ -255,6 +256,7 @@ "postcode-validator": "3.7.0", "preact": "^10.11.3", "react-number-format": "4.9.3", + "react-transition-group": "^4.4.5", "reakit": "1.3.11", "snakecase-keys": "5.4.2", "trim-html": "0.1.9", diff --git a/packages/checkout/components/store-notice/index.tsx b/packages/checkout/components/store-notice/index.tsx index b80c42dbcf0..7236125a447 100644 --- a/packages/checkout/components/store-notice/index.tsx +++ b/packages/checkout/components/store-notice/index.tsx @@ -2,24 +2,27 @@ * External dependencies */ import classnames from 'classnames'; -import { Notice } from 'wordpress-components'; -import { info, warning, Icon } from '@wordpress/icons'; +import NoticeBanner, { + NoticeBannerProps, +} from '@woocommerce/base-components/notice-banner'; /** - * Internal dependencies + * Wrapper for NoticeBanner component. */ -import './style.scss'; - -const StoreNotice = ( { className, children, status, ...props } ) => { +const StoreNotice = ( { + className, + children, + status, + ...props +}: NoticeBannerProps ) => { return ( - - -
{ children }
-
+ { children } + ); }; diff --git a/packages/checkout/components/store-notice/style.scss b/packages/checkout/components/store-notice/style.scss deleted file mode 100644 index 9816a3a878a..00000000000 --- a/packages/checkout/components/store-notice/style.scss +++ /dev/null @@ -1,51 +0,0 @@ -.wc-block-store-notice { - margin: $gap 0; - @include font-size(small); - padding: 0.5em; - border-radius: 4px; - border: 2px solid; - - .components-notice__content { - position: relative; - - > div { - padding-left: 2.5em; - } - svg { - vertical-align: middle; - width: 2em; - height: 2em; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } - } - - &.is-warning { - background-color: #fffdf9; - border-color: #fec; - - .components-notice__content svg { - fill: #f9b51f; - } - } - - &.is-info { - background-color: #e7f6f9; - border-color: #c8f6ff; - - .components-notice__content svg { - fill: #419ece; - } - } - - &.is-error { - background-color: #f8ebea; - border-color: #ffd4cd; - - .components-notice__content svg { - fill: #cd433b; - } - } -} diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index d1271174931..e84b6ffdecf 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -9,6 +9,7 @@ import { import { getNoticeContexts } from '@woocommerce/base-utils'; import type { Notice } from '@wordpress/notices'; import { useMemo, useEffect } from '@wordpress/element'; +import type { NoticeType } from '@woocommerce/types'; /** * Internal dependencies @@ -16,13 +17,13 @@ import { useMemo, useEffect } from '@wordpress/element'; import './style.scss'; import StoreNotices from './store-notices'; import SnackbarNotices from './snackbar-notices'; -import type { StoreNoticesContainerProps, StoreNotice } from './types'; +import type { StoreNoticesContainerProps } from './types'; -const formatNotices = ( notices: Notice[], context: string ): StoreNotice[] => { +const formatNotices = ( notices: Notice[], context: string ): NoticeType[] => { return notices.map( ( notice ) => ( { ...notice, context, - } ) ) as StoreNotice[]; + } ) ) as NoticeType[]; }; const StoreNoticesContainer = ( { @@ -57,7 +58,7 @@ const StoreNoticesContainer = ( { // Get notices from the current context and any sub-contexts and append the name of the context to the notice // objects for later reference. - const notices = useSelect< StoreNotice[] >( ( select ) => { + const notices = useSelect< NoticeType[] >( ( select ) => { const { getNotices } = select( 'core/notices' ); return [ @@ -70,7 +71,7 @@ const StoreNoticesContainer = ( { subContext ) ), - ].filter( Boolean ) as StoreNotice[]; + ].filter( Boolean ) as NoticeType[]; } ); // Register the container context with the parent. @@ -81,7 +82,7 @@ const StoreNoticesContainer = ( { }; }, [ contexts, registerContainer, unregisterContainer ] ); - if ( suppressNotices || ! notices.length ) { + if ( suppressNotices ) { return null; } diff --git a/packages/checkout/components/store-notices-container/snackbar-notices.tsx b/packages/checkout/components/store-notices-container/snackbar-notices.tsx index f5b591cbb75..448ca6441fa 100644 --- a/packages/checkout/components/store-notices-container/snackbar-notices.tsx +++ b/packages/checkout/components/store-notices-container/snackbar-notices.tsx @@ -2,39 +2,26 @@ * External dependencies */ import classnames from 'classnames'; -import { SnackbarList } from 'wordpress-components'; +import SnackbarList from '@woocommerce/base-components/snackbar-list'; import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import type { StoreNotice } from './types'; +import type { NoticeType } from '@woocommerce/types'; const SnackbarNotices = ( { className, notices, }: { className: string; - notices: StoreNotice[]; + notices: NoticeType[]; } ): JSX.Element | null => { const { removeNotice } = useDispatch( 'core/notices' ); - if ( ! notices.length ) { - return null; - } - return ( { - return { - ...notice, - className: 'components-snackbar--status-' + notice.status, - }; - } ) } + notices={ notices } onRemove={ ( noticeId: string ) => { notices.forEach( ( notice ) => { if ( notice.explicitDismiss && notice.id === noticeId ) { diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 73a6e29ad8a..8680fd7ae95 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -1,26 +1,27 @@ /** * External dependencies */ +import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; -import { useRef, useEffect } from '@wordpress/element'; -import { Notice } from 'wordpress-components'; +import { useRef, useEffect, RawHTML } from '@wordpress/element'; import { sanitizeHTML } from '@woocommerce/utils'; import { useDispatch } from '@wordpress/data'; import { usePrevious } from '@woocommerce/base-hooks'; import { decodeEntities } from '@wordpress/html-entities'; +import type { NoticeType } from '@woocommerce/types'; +import type { NoticeBannerProps } from '@woocommerce/base-components/notice-banner'; /** * Internal dependencies */ -import { getClassNameFromStatus } from './utils'; -import type { StoreNotice } from './types'; +import StoreNotice from '../store-notice'; const StoreNotices = ( { className, notices, }: { className: string; - notices: StoreNotice[]; + notices: NoticeType[]; } ): JSX.Element => { const ref = useRef< HTMLDivElement >( null ); const { removeNotice } = useDispatch( 'core/notices' ); @@ -79,6 +80,9 @@ const StoreNotices = ( { ( { status } ) => status === 'warning' ), info: dismissibleNotices.filter( ( { status } ) => status === 'info' ), + default: dismissibleNotices.filter( + ( { status } ) => status === 'default' + ), }; return ( @@ -87,70 +91,75 @@ const StoreNotices = ( { className={ classnames( className, 'wc-block-components-notices' ) } > { nonDismissibleNotices.map( ( notice ) => ( - - { sanitizeHTML( decodeEntities( notice.content ) ) } - + + { sanitizeHTML( decodeEntities( notice.content ) ) } + + ) ) } { Object.entries( dismissibleNoticeGroups ).map( ( [ status, noticeGroup ] ) => { if ( ! noticeGroup.length ) { return null; } - const uniqueNotices = noticeGroup.filter( - ( - notice: Notice, - noticeIndex: number, - noticesArray: Notice[] - ) => - noticesArray.findIndex( - ( _notice: Notice ) => - _notice.content === notice.content - ) === noticeIndex - ); - return ( - { - noticeGroup.forEach( ( notice ) => { - removeNotice( notice.id, notice.context ); - } ); - } } + const uniqueNotices = noticeGroup + .filter( + ( + notice: NoticeType, + noticeIndex: number, + noticesArray: NoticeType[] + ) => + noticesArray.findIndex( + ( _notice: NoticeType ) => + _notice.content === notice.content + ) === noticeIndex + ) + .map( ( notice ) => ( { + ...notice, + content: sanitizeHTML( + decodeEntities( notice.content ) + ), + } ) ); + const noticeProps: Omit< NoticeBannerProps, 'children' > & { + key: string; + } = { + key: `store-notice-${ status }`, + status: 'error', + onRemove: () => { + noticeGroup.forEach( ( notice ) => { + removeNotice( notice.id, notice.context ); + } ); + }, + }; + return uniqueNotices.length === 1 ? ( + + { noticeGroup[ 0 ].content } + + ) : ( + - { uniqueNotices.length === 1 ? ( - <> - { sanitizeHTML( - decodeEntities( - noticeGroup[ 0 ].content - ) - ) } - - ) : ( -
    - { uniqueNotices.map( ( notice ) => ( -
  • - { sanitizeHTML( - decodeEntities( notice.content ) - ) } -
  • - ) ) } -
- ) } -
+
    + { uniqueNotices.map( ( notice ) => ( +
  • + { notice.content } +
  • + ) ) } +
+ ); } ) } diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 999c1ac291d..e5970be7491 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -1,16 +1,11 @@ /** * External dependencies */ -import type { - Notice as NoticeType, - Options as NoticeOptions, -} from '@wordpress/notices'; +import type { NoticeType } from '@woocommerce/types'; export interface StoreNoticesContainerProps { className?: string | undefined; context?: string | string[]; // List of additional notices that were added inline and not stored in the `core/notices` store. - additionalNotices?: ( NoticeType & NoticeOptions )[]; + additionalNotices?: NoticeType[]; } - -export type StoreNotice = NoticeType & NoticeOptions; diff --git a/packages/checkout/components/store-notices-container/utils.ts b/packages/checkout/components/store-notices-container/utils.ts deleted file mode 100644 index 8565c4bb626..00000000000 --- a/packages/checkout/components/store-notices-container/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const getClassNameFromStatus = ( status = 'default' ): string => { - switch ( status ) { - case 'error': - return 'woocommerce-error'; - case 'success': - return 'woocommerce-message'; - case 'info': - case 'warning': - return 'woocommerce-info'; - } - return ''; -}; diff --git a/packages/checkout/utils/validation/is-postcode.ts b/packages/checkout/utils/validation/is-postcode.ts index 82ff5606916..bb3104f3d33 100644 --- a/packages/checkout/utils/validation/is-postcode.ts +++ b/packages/checkout/utils/validation/is-postcode.ts @@ -27,8 +27,10 @@ export interface IsPostcodeProps { } const isPostcode = ( { postcode, country }: IsPostcodeProps ): boolean => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return DEFAULT_REGEXES.get( country )!.test( postcode ); + // If the country is not in the list of regexes, trying to test it would result in an error, so we skip and assume + // that it is valid. + const postcodeTest = DEFAULT_REGEXES.get( country )?.test( postcode ); + return typeof postcodeTest !== 'undefined' ? postcodeTest : true; }; export default isPostcode; diff --git a/readme.txt b/readme.txt index eb86cb48b7b..ba5ad976b54 100644 --- a/readme.txt +++ b/readme.txt @@ -80,6 +80,45 @@ Release and roadmap notes available on the [WooCommerce Developers Blog](https:/ == Changelog == += 9.9.0 - 2023-03-30 = + +#### Enhancements + +- Move Related Products's notice component to the Inspector Control section. ([8843](https://github.com/woocommerce/woocommerce-blocks/pull/8843)) +- Product SKU Block: Don't render the prefix when the SKU isn't defined. ([8837](https://github.com/woocommerce/woocommerce-blocks/pull/8837)) +- Mini-cart: Add the option to change the style between 'Outline' and 'Fill' in all the buttons. ([8835](https://github.com/woocommerce/woocommerce-blocks/pull/8835)) +- Product SKU: Make the block focusable in editor. ([8804](https://github.com/woocommerce/woocommerce-blocks/pull/8804)) +- Add Fill & Outline styles, width settings & new typography controls for Product(Add to cart) button block. ([8781](https://github.com/woocommerce/woocommerce-blocks/pull/8781)) +- Allow themes to remove the Mini Cart title on overridden template parts. ([8779](https://github.com/woocommerce/woocommerce-blocks/pull/8779)) +- Add style to the `Mini Cart` buttons. ([8776](https://github.com/woocommerce/woocommerce-blocks/pull/8776)) +- Add the ability to change the background and text colors of the Mini Cart block "Start shopping" button. ([8766](https://github.com/woocommerce/woocommerce-blocks/pull/8766)) +- Mini-cart: Add setting to not render the block on the cart & checkout pages. ([8700](https://github.com/woocommerce/woocommerce-blocks/pull/8700)) +- Ensure shipping rates do not show in the Checkout block if the "Hide shipping costs until an address is entered option is selected". ([8682](https://github.com/woocommerce/woocommerce-blocks/pull/8682)) +- Move option to hide shipping costs until an address is entered to the Checkout block. ([8680](https://github.com/woocommerce/woocommerce-blocks/pull/8680)) +- Remove certain Shipping settings from WooCommerce -> Settings -> Shipping -> Shipping Options when using the Cart or Checkout blocks, these have been moved to setting on the blocks. ([8679](https://github.com/woocommerce/woocommerce-blocks/pull/8679)) +- Add spacing between Mini Cart title and products list when scrolled. ([8676](https://github.com/woocommerce/woocommerce-blocks/pull/8676)) +- Add new ExperimentalOrderLocalPickupPackages Slot/Fill. ([8636](https://github.com/woocommerce/woocommerce-blocks/pull/8636)) +- Add default Single Product HTML template. ([8515](https://github.com/woocommerce/woocommerce-blocks/pull/8515)) +- Validate postcodes client-site instead of server-side. ([8503](https://github.com/woocommerce/woocommerce-blocks/pull/8503)) +- Added support for `woocommerce_available_payment_gateways` to the Store API. ([8441](https://github.com/woocommerce/woocommerce-blocks/pull/8441)) + +#### Bug Fixes + +- Fix border styles not visible in the editor in Featured Product and Featured Category blocks. ([8838](https://github.com/woocommerce/woocommerce-blocks/pull/8838)) +- Add decoding to product names titles that are in HTML entities. ([8824](https://github.com/woocommerce/woocommerce-blocks/pull/8824)) +- Fix react 18 error in the editor when using cart/checkout blocks. ([8820](https://github.com/woocommerce/woocommerce-blocks/pull/8820)) +- Filter by Stock and Filter by Rating: Fix the potential endless redirection loop when used on a search results page. ([8784](https://github.com/woocommerce/woocommerce-blocks/pull/8784)) +- Fix "Save changes" default behavior bug in the Firefox browser. ([8754](https://github.com/woocommerce/woocommerce-blocks/pull/8754)) +- Store API - Apply `woocommerce_cart_item_permalink` filter to cart item permalinks. ([8726](https://github.com/woocommerce/woocommerce-blocks/pull/8726)) +- Add validation error to prevent checkout when there is no shipping method available. ([8384](https://github.com/woocommerce/woocommerce-blocks/pull/8384)) +- Fix Markdown table in payment-method-integration.md > external contribution #8158. ([8258](https://github.com/woocommerce/woocommerce-blocks/pull/8258)) + +#### Various + +- Display pickup location details in order confirmations. ([8727](https://github.com/woocommerce/woocommerce-blocks/pull/8727)) +- Local Pickup: Merge country and state into same field in location modal. ([8408](https://github.com/woocommerce/woocommerce-blocks/pull/8408)) +- Enable users to migrate to the blockified Single Product template. ([8324](https://github.com/woocommerce/woocommerce-blocks/pull/8324)) + = 9.8.4 - 2023-03-29 = #### Bug Fixes @@ -105,6 +144,41 @@ Release and roadmap notes available on the [WooCommerce Developers Blog](https:/ - Fix Customer Account block doing a 404 request in the frontend. ([8798](https://github.com/woocommerce/woocommerce-blocks/pull/8798)) - Fix issue that prevented spaces being added to Mini Cart, Cart and Checkout buttons in Firefox. ([8777](https://github.com/woocommerce/woocommerce-blocks/pull/8777)) += 9.9.0 - 2023-03-27 = + +#### Enhancements + +- Move Related Products's notice component to the Inspector Control section. ([8843](https://github.com/woocommerce/woocommerce-blocks/pull/8843)) +- Product SKU Block: Don't render the prefix when the SKU isn't defined. ([8837](https://github.com/woocommerce/woocommerce-blocks/pull/8837)) +- Mini-cart: Add the option to change the style between 'Outline' and 'Fill' in all the buttons. ([8835](https://github.com/woocommerce/woocommerce-blocks/pull/8835)) +- Product SKU: Make the block focusable in editor. ([8804](https://github.com/woocommerce/woocommerce-blocks/pull/8804)) +- Add Fill & Outline styles, width settings & new typography controls for Product(Add to cart) button block. ([8781](https://github.com/woocommerce/woocommerce-blocks/pull/8781)) +- Allow themes to remove the Mini Cart title on overridden template parts. ([8779](https://github.com/woocommerce/woocommerce-blocks/pull/8779)) +- Add style to the `Mini Cart` buttons. ([8776](https://github.com/woocommerce/woocommerce-blocks/pull/8776)) +- Add the ability to change the background and text colors of the Mini Cart block "Start shopping" button. ([8766](https://github.com/woocommerce/woocommerce-blocks/pull/8766)) +- Mini-cart: Add setting to not render the block on the cart & checkout pages. ([8700](https://github.com/woocommerce/woocommerce-blocks/pull/8700)) +- Ensure shipping rates do not show in the Checkout block if the "Hide shipping costs until an address is entered option is selected". ([8682](https://github.com/woocommerce/woocommerce-blocks/pull/8682)) +- Move option to hide shipping costs until an address is entered to the Checkout block. ([8680](https://github.com/woocommerce/woocommerce-blocks/pull/8680)) +- Remove certain Shipping settings from WooCommerce -> Settings -> Shipping -> Shipping Options when using the Cart or Checkout blocks, these have been moved to setting on the blocks. ([8679](https://github.com/woocommerce/woocommerce-blocks/pull/8679)) +- Add spacing between Mini Cart title and products list when scrolled. ([8676](https://github.com/woocommerce/woocommerce-blocks/pull/8676)) +- Add new ExperimentalOrderLocalPickupPackages Slot/Fill. ([8636](https://github.com/woocommerce/woocommerce-blocks/pull/8636)) +- Add default Single Product HTML template. ([8515](https://github.com/woocommerce/woocommerce-blocks/pull/8515)) +- Validate postcodes client-site instead of server-side. ([8503](https://github.com/woocommerce/woocommerce-blocks/pull/8503)) +- Added support for `woocommerce_available_payment_gateways` to the Store API. ([8441](https://github.com/woocommerce/woocommerce-blocks/pull/8441)) +- Display pickup location details in order confirmations. ([8727](https://github.com/woocommerce/woocommerce-blocks/pull/8727)) +- Local Pickup: Merge country and state into same field in location modal. ([8408](https://github.com/woocommerce/woocommerce-blocks/pull/8408)) +- Enable users to migrate to the blockified Single Product template. ([8324](https://github.com/woocommerce/woocommerce-blocks/pull/8324)) + +#### Bug Fixes + +- Fix border styles not visible in the editor in Featured Product and Featured Category blocks. ([8838](https://github.com/woocommerce/woocommerce-blocks/pull/8838)) +- Add decoding to product names titles that are in HTML entities. ([8824](https://github.com/woocommerce/woocommerce-blocks/pull/8824)) +- Fix react 18 error in the editor when using cart/checkout blocks. ([8820](https://github.com/woocommerce/woocommerce-blocks/pull/8820)) +- Filter by Stock and Filter by Rating: Fix the potential endless redirection loop when used on a search results page. ([8784](https://github.com/woocommerce/woocommerce-blocks/pull/8784)) +- Fix "Save changes" default behavior bug in the Firefox browser. ([8754](https://github.com/woocommerce/woocommerce-blocks/pull/8754)) +- Store API - Apply `woocommerce_cart_item_permalink` filter to cart item permalinks. ([8726](https://github.com/woocommerce/woocommerce-blocks/pull/8726)) +- Add validation error to prevent checkout when there is no shipping method available. ([8384](https://github.com/woocommerce/woocommerce-blocks/pull/8384)) + = 9.8.1 - 2023-03-15 = #### Bug Fixes @@ -112,6 +186,9 @@ Release and roadmap notes available on the [WooCommerce Developers Blog](https:/ - Fix Single Product page not visible in block themes that provided a custom template. ([8758](https://github.com/woocommerce/woocommerce-blocks/pull/8758)) - Products by Attributes: Fix the block rendered empty in the Editor. ([8759](https://github.com/woocommerce/woocommerce-blocks/pull/8759)) - Fix the local pickup price in the shipping type selector and pickup options. ([8623](https://github.com/woocommerce/woocommerce-blocks/pull/8623)) +- Enable users to migrate to the blockified Single Product template.([8902](https://github.com/woocommerce/woocommerce-blocks/pull/8902)) +- Fixed an issue where extensions were unable to programatically set the shipping address during payment processing. ([8878](https://github.com/woocommerce/woocommerce-blocks/pull/8878)) +- Fix unlinked border widths not being applied correctly in the frontend in WP 6.2 for some blocks. ([8893](https://github.com/woocommerce/woocommerce-blocks/pull/8893)) = 9.8.0 - 2023-03-14 = diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php index fbc7f99c768..faf6d49a8ed 100644 --- a/src/BlockTemplatesController.php +++ b/src/BlockTemplatesController.php @@ -2,7 +2,6 @@ namespace Automattic\WooCommerce\Blocks; use Automattic\WooCommerce\Blocks\Domain\Package; -use Automattic\WooCommerce\Blocks\Templates\BlockTemplatesCompatibility; use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate; use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils; diff --git a/src/BlockTypes/AddToCartForm.php b/src/BlockTypes/AddToCartForm.php index 6317d1b80ae..775876783ce 100644 --- a/src/BlockTypes/AddToCartForm.php +++ b/src/BlockTypes/AddToCartForm.php @@ -25,23 +25,34 @@ class AddToCartForm extends AbstractBlock { * @return string | void Rendered block output. */ protected function render( $attributes, $content, $block ) { - ob_start(); + global $product; + + $post_id = $block->context['postId']; + + if ( ! isset( $post_id ) ) { + return ''; + } - while ( have_posts() ) { - the_post(); - global $product; - /** - * Trigger the single product add to cart action for each product type. - * - * @since 9.7.0 - */ - do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); + if ( ! $product instanceof \WC_Product ) { + $product = wc_get_product( $post_id ); + if ( ! $product instanceof \WC_Product ) { + return ''; + } } + ob_start(); + + /** + * Trigger the single product add to cart action for each product type. + * + * @since 9.7.0 + */ + do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' ); + $product = ob_get_clean(); if ( ! $product ) { - return; + return ''; } $classname = $attributes['className'] ?? ''; diff --git a/src/BlockTypes/ProductPrice.php b/src/BlockTypes/ProductPrice.php index 795432374ad..997dd60ba12 100644 --- a/src/BlockTypes/ProductPrice.php +++ b/src/BlockTypes/ProductPrice.php @@ -82,7 +82,7 @@ protected function render( $attributes, $content, $block ) { $product = wc_get_product( $post_id ); if ( $product ) { - $styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array( 'font_size', 'font_weight', 'font_style', 'text_color', 'background_color', 'margin' ) ); + $styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes ); return sprintf( diff --git a/src/BlockTypes/ProductSKU.php b/src/BlockTypes/ProductSKU.php index b22f0d05609..bba01cbd0ac 100644 --- a/src/BlockTypes/ProductSKU.php +++ b/src/BlockTypes/ProductSKU.php @@ -1,6 +1,8 @@ + '
SKU: - %s + %3$s
', + esc_attr( $styles_and_classes['classes'] ), + esc_attr( $styles_and_classes['styles'] ?? '' ), $product_sku ); } diff --git a/src/BlockTypes/SingleProduct.php b/src/BlockTypes/SingleProduct.php new file mode 100644 index 00000000000..9d0e31da8f2 --- /dev/null +++ b/src/BlockTypes/SingleProduct.php @@ -0,0 +1,154 @@ + + %2$s + ', + esc_attr( $classname ), + $content + ); + + return $html; + } + + /** + * Update the context by injecting the correct post data + * for each one of the Single Product inner blocks. + * + * @param array $context Block context. + * @param array $block Block attributes. + * @param WP_Block $parent_block Block instance. + * + * @return array Updated block context. + */ + public function update_context( $context, $block, $parent_block ) { + if ( 'woocommerce/single-product' == $block['blockName'] + && isset( $block['attrs']['productId'] ) ) { + $this->product_id = $block['attrs']['productId']; + + $this->single_product_inner_blocks_names = array_reverse( + $this->extract_single_product_inner_block_names( $block ) + ); + } + + $this->replace_post_for_single_product_inner_block( $block, $context ); + + return $context; + } + + /** + * Extract the inner block names for the Single Product block. This way it's possible + * to map all the inner blocks for a Single Product block and manipulate the data as needed. + * + * @param array $block The Single Product block or its inner blocks. + * @param array $result Array of inner block names. + * + * @return array Array containing all the inner block names of a Single Product block. + */ + protected function extract_single_product_inner_block_names( $block, &$result = [] ) { + if ( isset( $block['blockName'] ) ) { + $result[] = $block['blockName']; + } + + if ( isset( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $inner_block ) { + $this->extract_single_product_inner_block_names( $inner_block, $result ); + } + } + return $result; + } + + /** + * Replace the global post for the Single Product inner blocks and reset it after. + * + * This is needed because some of the inner blocks may use the global post + * instead of fetching the product through the `productId` attribute, so even if the + * `productId` is passed to the inner block, it will still use the global post. + * + * @param array $block Block attributes. + * @param array $context Block context. + */ + protected function replace_post_for_single_product_inner_block( $block, &$context ) { + if ( $this->single_product_inner_blocks_names ) { + $block_name = array_pop( $this->single_product_inner_blocks_names ); + + if ( $block_name === $block['blockName'] ) { + // @todo This is a temporary fix to make the Post Excerpt block work while https://github.com/WordPress/gutenberg/pull/49495 is not merged + if ( 'core/post-excerpt' === $block_name ) { + global $post; + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post = get_post( $this->product_id ); + setup_postdata( $post ); + } + $context['postId'] = $this->product_id; + } + + if ( ! $this->single_product_inner_blocks_names ) { + wp_reset_postdata(); + } + } + } + + /** + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + * + * @return null This block has no frontend script. + */ + protected function get_block_type_script( $key = null ) { + return null; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 04846b9869b..9f2296a0f55 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -223,6 +223,10 @@ protected function get_block_types() { MiniCartContents::get_mini_cart_block_types() ); + if ( Package::feature()->is_experimental_build() ) { + $block_types[] = 'SingleProduct'; + } + /** * This disables specific blocks in Widget Areas by not registering them. */ diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index 8ef3f15dc46..f50b6597cf8 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -8,6 +8,7 @@ use Automattic\WooCommerce\Blocks\BlockTemplatesController; use Automattic\WooCommerce\Blocks\BlockTypesController; use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount; +use Automattic\WooCommerce\Blocks\Domain\Services\Notices; use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders; use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics; @@ -124,6 +125,7 @@ function() { } $this->container->get( DraftOrders::class )->init(); $this->container->get( CreateAccount::class )->init(); + $this->container->get( Notices::class )->init(); $this->container->get( StoreApi::class )->init(); $this->container->get( GoogleAnalytics::class ); $this->container->get( BlockTypesController::class ); @@ -314,6 +316,12 @@ function( Container $container ) { return new GoogleAnalytics( $asset_api ); } ); + $this->container->register( + Notices::class, + function( Container $container ) { + return new Notices( $container->get( Package::class ) ); + } + ); $this->container->register( PaymentsApi::class, function ( Container $container ) { diff --git a/src/Domain/Services/Notices.php b/src/Domain/Services/Notices.php new file mode 100644 index 00000000000..990681489b0 --- /dev/null +++ b/src/Domain/Services/Notices.php @@ -0,0 +1,111 @@ +package = $package; + } + + /** + * Set all hooks related to adding Checkout Draft order functionality to Woo Core. This is only enabled if the user + * is using the new block based cart/checkout. + */ + public function init() { + // Core page IDs. + $cart_page_id = wc_get_page_id( 'cart' ); + $checkout_page_id = wc_get_page_id( 'checkout' ); + + // Checks a specific page (by ID) to see if it contains the named block. + $has_block_cart = $cart_page_id && has_block( 'woocommerce/cart', $cart_page_id ); + $has_block_checkout = $checkout_page_id && has_block( 'woocommerce/checkout', $checkout_page_id ); + + if ( $has_block_cart || $has_block_checkout ) { + add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] ); + add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 ); + add_action( + 'wp_head', + function() { + // These pages may return notices in ajax responses, so we need the styles to be ready. + if ( is_cart() || is_checkout() ) { + wp_enqueue_style( 'wc-blocks-style' ); + } + } + ); + } + } + + /** + * Allow SVG icon in notices. + * + * @param array $allowed_tags Allowed tags. + * @return array + */ + public function add_kses_notice_allowed_tags( $allowed_tags ) { + $svg_args = array( + 'svg' => array( + 'aria-hidden' => true, + 'xmlns' => true, + 'width' => true, + 'height' => true, + 'viewbox' => true, + 'focusable' => true, + ), + 'path' => array( + 'd' => true, + ), + ); + return array_merge( $allowed_tags, $svg_args ); + } + + /** + * Replaces core notice templates with those from blocks. + * + * The new notice templates match block components with matching icons and styling. The only difference is that core + * only has notices for info, success, and error notices, whereas blocks has notices for info, success, error, + * warning, and a default notice type. + * + * @param string $template Located template path. + * @param string $template_name Template name. + * @param array $args Template arguments. + * @param string $template_path Template path. + * @param string $default_path Default path. + * @return string + */ + public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) { + if ( in_array( $template_name, $this->notice_templates, true ) ) { + $template = $this->package->get_path( 'templates/' . $template_name ); + wp_enqueue_style( 'wc-blocks-style' ); + } + return $template; + } +} diff --git a/src/Package.php b/src/Package.php index 5eaf1158466..6bb4fc5fcca 100644 --- a/src/Package.php +++ b/src/Package.php @@ -109,7 +109,7 @@ public static function container( $reset = false ) { NewPackage::class, function ( $container ) { // leave for automated version bumping. - $version = '9.9.0-dev'; + $version = '10.0.0-dev'; return new NewPackage( $version, dirname( __DIR__ ), diff --git a/src/Shipping/ShippingController.php b/src/Shipping/ShippingController.php index 6e313d99a41..e6be896593f 100644 --- a/src/Shipping/ShippingController.php +++ b/src/Shipping/ShippingController.php @@ -51,6 +51,7 @@ function() { true ); } + $this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ), true ); $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' ); add_action( 'rest_api_init', [ $this, 'register_settings' ] ); diff --git a/src/Templates/ArchiveProductTemplatesCompatibility.php b/src/Templates/ArchiveProductTemplatesCompatibility.php index fe06dbdf6c0..82846cfa7fa 100644 --- a/src/Templates/ArchiveProductTemplatesCompatibility.php +++ b/src/Templates/ArchiveProductTemplatesCompatibility.php @@ -23,14 +23,6 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility */ protected $hook_data; - /** - * Constructor. - */ - public function __construct() { - $this->set_hook_data(); - $this->init(); - } - /** * Update the render block data to inject our custom attribute needed to * determine which blocks belong to an inherited Products block. @@ -188,7 +180,7 @@ protected function set_hook_data() { ), 'woocommerce_after_shop_loop_item_title' => array( 'block_name' => 'core/post-title', - 'position' => 'before', + 'position' => 'after', 'hooked' => array( 'woocommerce_template_loop_rating' => 5, 'woocommerce_template_loop_price' => 10, diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php index 20d43943678..a0f1c636c19 100644 --- a/src/Utils/BlockTemplateUtils.php +++ b/src/Utils/BlockTemplateUtils.php @@ -201,7 +201,7 @@ public static function build_template_result_from_file( $template_file, $templat // Remove the term description block from the archive-product template // as the Product Catalog/Shop page doesn't have a description. if ( 'archive-product' === $template_file->slug ) { - $template->content = str_replace( '', '', $template->content ); + $template->content = str_replace( '', '', $template->content ); } // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909. $template->source = $template_file->source ? $template_file->source : 'plugin'; diff --git a/templates/notices/error.php b/templates/notices/error.php new file mode 100644 index 00000000000..436430d3c65 --- /dev/null +++ b/templates/notices/error.php @@ -0,0 +1,50 @@ + 1; + +?> + + + + +
role="alert"> + +
+ +
+
+ diff --git a/templates/notices/success.php b/templates/notices/success.php new file mode 100644 index 00000000000..a6d459663a3 --- /dev/null +++ b/templates/notices/success.php @@ -0,0 +1,29 @@ + + + +
role="alert"> + +
+ +
+
+ diff --git a/templates/templates/blockified/archive-product.html b/templates/templates/blockified/archive-product.html index 78b4ad4181b..46b75ec21f2 100644 --- a/templates/templates/blockified/archive-product.html +++ b/templates/templates/blockified/archive-product.html @@ -2,22 +2,23 @@
- -
- + + + - + - + - + +
+ + +
+ + +
- -
- - -
- diff --git a/templates/templates/blockified/product-search-results.html b/templates/templates/blockified/product-search-results.html index 2632197660e..5821e5c4973 100644 --- a/templates/templates/blockified/product-search-results.html +++ b/templates/templates/blockified/product-search-results.html @@ -2,21 +2,20 @@
- -
- - - + - + - -
- - -
- + + +
+ + +
+ + +
diff --git a/tests/e2e/config/custom-matchers/to-render-block.js b/tests/e2e/config/custom-matchers/to-render-block.js index cb6eb518baa..562b2c087ec 100644 --- a/tests/e2e/config/custom-matchers/to-render-block.js +++ b/tests/e2e/config/custom-matchers/to-render-block.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { getBlocksBySlug } from '../../utils.js'; + expect.extend( { async toRenderBlock( page, block = {} ) { const gutenbergNotFoundError = ( await page.content() ).match( @@ -39,11 +44,11 @@ expect.extend( { }; } - const blockElement = await page.$( block.class ); - if ( blockElement === null ) { + const blocks = await getBlocksBySlug( block.slug ); + if ( blocks >= 1 ) { return { message: () => - `the ${ block.name || 'block' } with classname \`${ + `${ block.name || 'block' } with classname \`${ block.class }\` did not render.`, pass: false, diff --git a/tests/e2e/config/jest.setup.js b/tests/e2e/config/jest.setup.js index 2ce19f2c536..002d3825225 100644 --- a/tests/e2e/config/jest.setup.js +++ b/tests/e2e/config/jest.setup.js @@ -12,6 +12,7 @@ import { } from '@wordpress/e2e-test-utils'; import { setDefaultOptions } from 'expect-puppeteer'; import { get } from 'lodash'; + /** * Internal dependencies */ diff --git a/tests/e2e/fixtures/fixture-data.js b/tests/e2e/fixtures/fixture-data.js index 97194702bb5..1ad81f5a625 100644 --- a/tests/e2e/fixtures/fixture-data.js +++ b/tests/e2e/fixtures/fixture-data.js @@ -278,7 +278,7 @@ const Settings = () => [ }, { id: 'woocommerce_specific_allowed_countries', - value: [ 'DZ', 'CA', 'NZ', 'ES', 'GB', 'US' ], + value: [ 'AL', 'DZ', 'CA', 'NZ', 'ES', 'GB', 'US' ], }, { id: 'woocommerce_ship_to_countries', @@ -286,7 +286,7 @@ const Settings = () => [ }, { id: 'woocommerce_specific_ship_to_countries', - value: [ 'DZ', 'CA', 'NZ', 'ES', 'GB', 'US' ], + value: [ 'AL', 'DZ', 'CA', 'NZ', 'ES', 'GB', 'US' ], }, { id: 'woocommerce_enable_coupons', diff --git a/tests/e2e/specs/backend/active-filters.test.js b/tests/e2e/specs/backend/active-filters.test.js index 7cfd2bff722..c912b97df43 100644 --- a/tests/e2e/specs/backend/active-filters.test.js +++ b/tests/e2e/specs/backend/active-filters.test.js @@ -2,16 +2,19 @@ * External dependencies */ import { + switchBlockInspectorTab, switchUserToAdmin, - openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; - import { visitBlockPage, selectBlockByName, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; +/** + * Internal dependencies + */ +import { openSettingsSidebar } from '../../utils.js'; + const block = { name: 'Active Filters', slug: 'woocommerce/active-filters', @@ -31,9 +34,9 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( block.slug ); + await switchBlockInspectorTab( 'Settings' ); } ); it( "allows changing the block's title", async () => { diff --git a/tests/e2e/specs/backend/attribute-filter.test.js b/tests/e2e/specs/backend/attribute-filter.test.js index 4bda1c37ef2..5b42cb1f7a0 100644 --- a/tests/e2e/specs/backend/attribute-filter.test.js +++ b/tests/e2e/specs/backend/attribute-filter.test.js @@ -2,17 +2,21 @@ * External dependencies */ import { + switchBlockInspectorTab, switchUserToAdmin, - openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; import { visitBlockPage, saveOrPublish, selectBlockByName, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; +/** + * Internal dependencies + */ +import { openSettingsSidebar } from '../../utils.js'; + const block = { name: 'Filter by Attribute', slug: 'woocommerce/attribute-filter', @@ -63,9 +67,9 @@ describe( `${ block.name } Block`, () => { describe( 'Attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); + await openSettingsSidebar(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await switchBlockInspectorTab( 'Settings' ); } ); it( "allows changing the block's title", async () => { diff --git a/tests/e2e/specs/backend/cart.test.js b/tests/e2e/specs/backend/cart.test.js index 5e177dd4f99..3e5ad553748 100644 --- a/tests/e2e/specs/backend/cart.test.js +++ b/tests/e2e/specs/backend/cart.test.js @@ -3,7 +3,6 @@ */ import { clickBlockToolbarButton, - openDocumentSettingsSidebar, switchUserToAdmin, searchForBlock, openGlobalBlockInserter, @@ -13,20 +12,25 @@ import { findLabelWithText, visitBlockPage, selectBlockByName, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { merchant } from '@woocommerce/e2e-utils'; /** * Internal dependencies */ -import { openWidgetEditor, closeModalIfExists } from '../../utils.js'; +import { + openSettingsSidebar, + openWidgetEditor, + closeModalIfExists, +} from '../../utils.js'; const block = { name: 'Cart', slug: 'woocommerce/cart', class: '.wp-block-woocommerce-cart', selectors: { + disabledInsertButton: + "//button[@aria-disabled='true']//span[text()='Cart']", insertButton: "//button//span[text()='Cart']", }, }; @@ -107,14 +111,22 @@ describe( `${ block.name } Block`, () => { '//div[@data-type="woocommerce/cart-order-summary-block"]//button[@aria-label="Add block"]' ); await addBlockButton.click(); + await expect( page ).toFill( + 'input.components-search-control__input', + 'Table' + ); const tableButton = await page.waitForXPath( '//*[@role="option" and contains(., "Table")]' ); + await expect( tableButton ).not.toBeNull(); + await expect( page ).toFill( + 'input.components-search-control__input', + 'Audio' + ); const audioButton = await page.waitForXPath( '//*[@role="option" and contains(., "Audio")]' ); - expect( tableButton ).not.toBeNull(); - expect( audioButton ).not.toBeNull(); + await expect( audioButton ).not.toBeNull(); // // Now check the filled cart block and expect only the Table block to be available there. await selectBlockByName( 'woocommerce/filled-cart-block' ); @@ -122,13 +134,14 @@ describe( `${ block.name } Block`, () => { '//div[@data-type="woocommerce/filled-cart-block"]//button[@aria-label="Add block"]' ); await filledCartAddBlockButton.click(); + const filledCartTableButton = await page.waitForXPath( '//*[@role="option" and contains(., "Table")]' ); + expect( filledCartTableButton ).not.toBeNull(); const filledCartAudioButton = await page.$x( '//*[@role="option" and contains(., "Audio")]' ); - expect( filledCartTableButton ).not.toBeNull(); expect( filledCartAudioButton ).toHaveLength( 0 ); } ); @@ -179,17 +192,14 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( - 'Settings' - ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/cart-order-summary-shipping-block' ); } ); it( 'can toggle Shipping calculator', async () => { - const selector = ` .wc-block-components-totals-shipping__change-address__link`; + const selector = `.wc-block-components-totals-shipping__change-address__link`; const toggleLabel = await findLabelWithText( 'Shipping calculator' ); diff --git a/tests/e2e/specs/backend/checkout.test.js b/tests/e2e/specs/backend/checkout.test.js index 8d30fc18881..28cc2296609 100644 --- a/tests/e2e/specs/backend/checkout.test.js +++ b/tests/e2e/specs/backend/checkout.test.js @@ -2,7 +2,6 @@ * External dependencies */ import { - openDocumentSettingsSidebar, switchUserToAdmin, openGlobalBlockInserter, insertBlock, @@ -11,7 +10,6 @@ import { findLabelWithText, visitBlockPage, selectBlockByName, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { merchant } from '@woocommerce/e2e-utils'; @@ -20,6 +18,7 @@ import { merchant } from '@woocommerce/e2e-utils'; */ import { searchForBlock, + openSettingsSidebar, openWidgetEditor, closeModalIfExists, } from '../../utils.js'; @@ -125,10 +124,7 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( - 'Settings' - ); + await openSettingsSidebar(); await selectBlockByName( block.slug ); } ); @@ -163,7 +159,7 @@ describe( `${ block.name } Block`, () => { '.wc-block-checkout__shipping-method button', { text: 'Shipping' } ); - await openDocumentSettingsSidebar(); + await openSettingsSidebar(); const toggleLabel = await findLabelWithText( 'Hide shipping costs until an address is entered' ); @@ -225,10 +221,7 @@ describe( `${ block.name } Block`, () => { describe( 'shipping address block attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( - 'Settings' - ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-address-block' ); @@ -277,10 +270,7 @@ describe( `${ block.name } Block`, () => { describe( 'action block attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( - 'Settings' - ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-actions-block' ); } ); diff --git a/tests/e2e/specs/backend/customer-account.test.js b/tests/e2e/specs/backend/customer-account.test.js index 073eb8b4996..1b8c47c63ef 100644 --- a/tests/e2e/specs/backend/customer-account.test.js +++ b/tests/e2e/specs/backend/customer-account.test.js @@ -2,13 +2,15 @@ * External dependencies */ import { + switchBlockInspectorTab, switchUserToAdmin, - openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; -import { - visitBlockPage, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { visitBlockPage } from '@woocommerce/blocks-test-utils'; + +/** + * Internal dependencies + */ +import { openSettingsSidebar } from '../../utils.js'; const block = { name: 'Customer account', @@ -35,8 +37,8 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); + await switchBlockInspectorTab( 'Settings' ); await page.click( block.class ); } ); diff --git a/tests/e2e/specs/backend/price-filter.test.js b/tests/e2e/specs/backend/price-filter.test.js index cc142e7e95e..0c9d3a1db96 100644 --- a/tests/e2e/specs/backend/price-filter.test.js +++ b/tests/e2e/specs/backend/price-filter.test.js @@ -2,15 +2,19 @@ * External dependencies */ import { - openDocumentSettingsSidebar, + switchBlockInspectorTab, switchUserToAdmin, } from '@wordpress/e2e-test-utils'; import { visitBlockPage, selectBlockByName, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; +/** + * Internal dependencies + */ +import { openSettingsSidebar } from '../../utils.js'; + const block = { name: 'Filter by Price', slug: 'woocommerce/price-filter', @@ -30,11 +34,9 @@ describe( `${ block.name } Block`, () => { describe( 'Attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( - 'Settings' - ); + await openSettingsSidebar(); await selectBlockByName( block.slug ); + await switchBlockInspectorTab( 'Settings' ); } ); it( "allows changing the block's title", async () => { diff --git a/tests/e2e/specs/backend/product-categories.test.js b/tests/e2e/specs/backend/product-categories.test.js index 058692eeeca..6b607a0b13a 100644 --- a/tests/e2e/specs/backend/product-categories.test.js +++ b/tests/e2e/specs/backend/product-categories.test.js @@ -2,7 +2,6 @@ * External dependencies */ import { getAllBlocks, switchUserToAdmin } from '@wordpress/e2e-test-utils'; - import { visitBlockPage } from '@woocommerce/blocks-test-utils'; /** diff --git a/tests/e2e/specs/backend/product-query.test.ts b/tests/e2e/specs/backend/product-query.test.ts index 5e2dd9dfd75..e7231eb1bf6 100644 --- a/tests/e2e/specs/backend/product-query.test.ts +++ b/tests/e2e/specs/backend/product-query.test.ts @@ -5,13 +5,9 @@ import { getAllBlocks, switchUserToAdmin, canvas, - openDocumentSettingsSidebar, openListView, } from '@wordpress/e2e-test-utils'; -import { - visitBlockPage, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { visitBlockPage } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies @@ -20,6 +16,7 @@ import { insertBlockDontWaitForInsertClose, GUTENBERG_EDITOR_CONTEXT, describeOrSkip, + openSettingsSidebar, } from '../../utils'; const block = { @@ -53,8 +50,7 @@ describeOrSkip( GUTENBERG_EDITOR_CONTEXT === 'gutenberg' )( it.skip( 'Editor preview shows only on sale products after enabling `Show only products on sale`', async () => { await visitBlockPage( `${ block.name } Block` ); const canvasEl = canvas(); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await openListView(); await page.click( '.block-editor-list-view-block__contents-container a.components-button' diff --git a/tests/e2e/specs/backend/product-query/advanced-filters.test.ts b/tests/e2e/specs/backend/product-query/advanced-filters.test.ts index 36d4e45b34e..f7325217d41 100644 --- a/tests/e2e/specs/backend/product-query/advanced-filters.test.ts +++ b/tests/e2e/specs/backend/product-query/advanced-filters.test.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import { ensureSidebarOpened, canvas } from '@wordpress/e2e-test-utils'; import { saveOrPublish, selectBlockByName, @@ -8,11 +9,9 @@ import { getFixtureProductsData, shopper, getToggleIdByLabel, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { ElementHandle } from 'puppeteer'; import { setCheckbox } from '@woocommerce/e2e-utils'; -import { ensureSidebarOpened, canvas } from '@wordpress/e2e-test-utils'; /** * Internal dependencies @@ -48,7 +47,6 @@ describeOrSkip( GUTENBERG_EDITOR_CONTEXT === 'gutenberg' )( await resetProductQueryBlockPage(); await ensureSidebarOpened(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); $productFiltersPanel = await findToolsPanelWithTitle( 'Advanced Filters' ); diff --git a/tests/e2e/specs/backend/product-query/popular-filters.test.ts b/tests/e2e/specs/backend/product-query/popular-filters.test.ts index 0a393d29e76..86bb8241716 100644 --- a/tests/e2e/specs/backend/product-query/popular-filters.test.ts +++ b/tests/e2e/specs/backend/product-query/popular-filters.test.ts @@ -11,7 +11,6 @@ import { selectBlockByName, visitBlockPage, saveOrPublish, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; /** @@ -29,7 +28,6 @@ import { const getPopularFilterPanel = async () => { await ensureSidebarOpened(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); return await findSidebarPanelWithTitle( 'Popular Filters' ); }; diff --git a/tests/e2e/specs/backend/rating-filter.test.js b/tests/e2e/specs/backend/rating-filter.test.js index 1bd04ea9106..324b37edf68 100644 --- a/tests/e2e/specs/backend/rating-filter.test.js +++ b/tests/e2e/specs/backend/rating-filter.test.js @@ -2,17 +2,15 @@ * External dependencies */ import { + switchBlockInspectorTab, switchUserToAdmin, - openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; -import { - visitBlockPage, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { visitBlockPage } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ +import { openSettingsSidebar } from '../../utils'; const block = { name: 'Filter by Rating', @@ -32,9 +30,9 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await page.click( block.class ); + await switchBlockInspectorTab( 'Settings' ); } ); it( 'product count can be toggled', async () => { diff --git a/tests/e2e/specs/backend/site-editing-templates.test.js b/tests/e2e/specs/backend/site-editing-templates.test.js index ccc033c3ae7..7abef3e64b9 100644 --- a/tests/e2e/specs/backend/site-editing-templates.test.js +++ b/tests/e2e/specs/backend/site-editing-templates.test.js @@ -128,7 +128,7 @@ const SELECTORS = { const CUSTOMIZED_STRING = 'My awesome customization'; const WOOCOMMERCE_ID = 'woocommerce/woocommerce'; -const WOOCOMMERCE_PARSED_ID = 'WooCommerce'; +const WOOCOMMERCE_PARSED_ID = 'woocommerce/woocommerceCustomized'; describe( 'Store Editing Templates', () => { useTheme( 'emptytheme' ); diff --git a/tests/e2e/specs/backend/stock-filter.test.js b/tests/e2e/specs/backend/stock-filter.test.js index a8f65050fd5..626899eeb43 100644 --- a/tests/e2e/specs/backend/stock-filter.test.js +++ b/tests/e2e/specs/backend/stock-filter.test.js @@ -2,17 +2,18 @@ * External dependencies */ import { + switchBlockInspectorTab, switchUserToAdmin, - openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; -import { - visitBlockPage, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { visitBlockPage } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ +/** + * Internal dependencies + */ +import { openSettingsSidebar } from '../../utils'; import { findLabelWithText } from '../../../utils'; const block = { @@ -33,9 +34,9 @@ describe( `${ block.name } Block`, () => { describe( 'attributes', () => { beforeEach( async () => { - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await page.click( block.class ); + await switchBlockInspectorTab( 'Settings' ); } ); it( 'product count can be toggled', async () => { diff --git a/tests/e2e/specs/merchant/checkout-terms.test.js b/tests/e2e/specs/merchant/checkout-terms.test.js index 70824729214..0b92e0828ec 100644 --- a/tests/e2e/specs/merchant/checkout-terms.test.js +++ b/tests/e2e/specs/merchant/checkout-terms.test.js @@ -1,22 +1,17 @@ /** * External dependencies */ -import { - merchant, - openDocumentSettingsSidebar, - setCheckbox, - unsetCheckbox, -} from '@woocommerce/e2e-utils'; +import { merchant, setCheckbox, unsetCheckbox } from '@woocommerce/e2e-utils'; import { visitBlockPage, selectBlockByName, saveOrPublish, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ +import { openSettingsSidebar } from '../../utils'; import { shopper, preventCompatibilityNotice, @@ -66,8 +61,7 @@ describe( 'Merchant → Checkout → Can adjust T&S and Privacy Policy options', await preventCompatibilityNotice(); await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-terms-block' ); const [ termsCheckboxLabel ] = await page.$x( `//label[contains(text(), "Require checkbox") and contains(@class, "components-toggle-control__label")]` @@ -106,8 +100,7 @@ describe( 'Merchant → Checkout → Can adjust T&S and Privacy Policy options', // Deactivate checkboxes for T&S and Privacy Policy links. await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-terms-block' ); await unsetCheckbox( termsCheckboxId ); await saveOrPublish(); diff --git a/tests/e2e/specs/shopper/active-filters.test.ts b/tests/e2e/specs/shopper/active-filters.test.ts index 71752d12cee..1793f050bcd 100644 --- a/tests/e2e/specs/shopper/active-filters.test.ts +++ b/tests/e2e/specs/shopper/active-filters.test.ts @@ -11,13 +11,13 @@ import { } from '@wordpress/e2e-test-utils'; import { SHOP_PAGE } from '@woocommerce/e2e-utils'; import { Frame, Page } from 'puppeteer'; -import { insertBlockUsingSlash } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ import { goToTemplateEditor, + insertAllProductsBlock, useTheme, saveTemplate, waitForAllProductsBlockLoaded, @@ -96,7 +96,7 @@ describe( 'Shopper → Active Filters Block', () => { } ); await insertBlocks(); - await insertBlockUsingSlash( 'All Products' ); + await insertAllProductsBlock(); await configureAttributeFilterBlock( page ); await publishPost(); @@ -182,7 +182,7 @@ describe( 'Shopper → Active Filters Block', () => { expect( isRefreshed ).not.toHaveBeenCalled(); } ); - it( 'Clicking "Clear All" button removes all active filter', async () => { + it( 'Clicking "Clear All" button removes all active filters', async () => { const isRefreshed = jest.fn( () => void 0 ); await page.waitForSelector( block.class ); await page.waitForSelector( @@ -317,7 +317,7 @@ describe( 'Shopper → Active Filters Block', () => { await expect( page ).toMatch( SIMPLE_PHYSICAL_PRODUCT_NAME ); } ); - it( 'Clicking "Clear All" button removes all active filter and the page redirects to the base URL', async () => { + it( 'Clicking "Clear All" button removes all active filters and the page redirects to the base URL', async () => { const isRefreshed = jest.fn( () => void 0 ); page.on( 'load', isRefreshed ); await page.waitForSelector( selectors.frontend.stockFilterBlock ); diff --git a/tests/e2e/specs/shopper/cart-checkout/checkout.test.js b/tests/e2e/specs/shopper/cart-checkout/checkout.test.js index f1e9e726db7..c0cf8483e9a 100644 --- a/tests/e2e/specs/shopper/cart-checkout/checkout.test.js +++ b/tests/e2e/specs/shopper/cart-checkout/checkout.test.js @@ -3,7 +3,6 @@ */ import { merchant, - openDocumentSettingsSidebar, setCheckbox, unsetCheckbox, withRestApi, @@ -13,9 +12,9 @@ import { selectBlockByName, saveOrPublish, getToggleIdByLabel, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { visitAdminPage } from '@wordpress/e2e-test-utils'; + /** * Internal dependencies */ @@ -30,7 +29,7 @@ import { BASE_URL, } from '../../../../utils'; import { merchant as merchantUtils } from '../../../../utils/merchant'; -import { createCoupon } from '../../../utils'; +import { createCoupon, openSettingsSidebar } from '../../../utils'; let coupon; @@ -154,8 +153,7 @@ describe( 'Shopper → Checkout', () => { await preventCompatibilityNotice(); await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-address-block' ); @@ -168,8 +166,7 @@ describe( 'Shopper → Checkout', () => { afterAll( async () => { await shopper.block.emptyCart(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-address-block' ); @@ -200,6 +197,36 @@ describe( 'Shopper → Checkout', () => { await shopper.block.verifyShippingDetails( SHIPPING_DETAILS ); await shopper.block.verifyBillingDetails( BILLING_DETAILS ); } ); + it( 'User can add postcodes for different countries', async () => { + await shopper.block.goToShop(); + await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME ); + await shopper.block.goToCheckout(); + await page.waitForSelector( + '.wc-block-checkout__use-address-for-billing input[type="checkbox"]' + ); + await unsetCheckbox( + '.wc-block-checkout__use-address-for-billing input[type="checkbox"]' + ); + await shopper.block.fillShippingDetails( { + ...SHIPPING_DETAILS, + country: 'Albania', + state: 'Berat', + postcode: '1234', + } ); + + await shopper.block.fillBillingDetails( { + ...BILLING_DETAILS, + country: 'United Kingdom', + postcode: 'SW1 1AA', + } ); + + await expect( page ).not.toMatchElement( + '.wc-block-components-validation-error p', + { + text: 'Please enter a valid postcode', + } + ); + } ); } ); describe( 'Checkout Form Errors', () => { @@ -310,8 +337,7 @@ describe( 'Shopper → Checkout', () => { afterAll( async () => { await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-methods-block' ); @@ -379,8 +405,7 @@ describe( 'Shopper → Checkout', () => { await preventCompatibilityNotice(); await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-methods-block' ); @@ -438,8 +463,7 @@ describe( 'Shopper → Checkout', () => { await preventCompatibilityNotice(); await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-methods-block' ); @@ -469,8 +493,7 @@ describe( 'Shopper → Checkout', () => { await merchantUtils.enableLocalPickup(); await merchantUtils.addLocalPickupLocation(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-methods-block' ); @@ -514,8 +537,7 @@ describe( 'Shopper → Checkout', () => { await preventCompatibilityNotice(); await merchant.login(); await visitBlockPage( 'Checkout Block' ); - await openDocumentSettingsSidebar(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await openSettingsSidebar(); await selectBlockByName( 'woocommerce/checkout-shipping-methods-block' ); diff --git a/tests/e2e/specs/shopper/filter-products-by-attribute.test.ts b/tests/e2e/specs/shopper/filter-products-by-attribute.test.ts index 2431491f3df..74400fa166c 100644 --- a/tests/e2e/specs/shopper/filter-products-by-attribute.test.ts +++ b/tests/e2e/specs/shopper/filter-products-by-attribute.test.ts @@ -8,23 +8,21 @@ import { insertBlock, switchUserToAdmin, publishPost, - ensureSidebarOpened, } from '@wordpress/e2e-test-utils'; -import { - selectBlockByName, - insertBlockUsingSlash, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { selectBlockByName } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ import { BASE_URL, + enableApplyFiltersButton, goToTemplateEditor, + insertAllProductsBlock, saveTemplate, useTheme, waitForAllProductsBlockLoaded, + waitForCanvas, } from '../../utils'; import { saveOrPublish } from '../../../utils'; @@ -36,8 +34,6 @@ const block = { editor: { firstAttributeInTheList: '.woocommerce-search-list__list > li > label > input.woocommerce-search-list__item-input', - filterButtonToggle: - '//label[text()="Show \'Apply filters\' button"]', doneButton: '.wc-block-attribute-filter__selection > button', }, frontend: { @@ -71,7 +67,7 @@ describe( `${ block.name } Block`, () => { title: block.name, } ); - await insertBlockUsingSlash( 'All Products' ); + await insertAllProductsBlock(); await insertBlock( block.name ); const canvasEl = canvas(); @@ -190,14 +186,9 @@ describe( `${ block.name } Block`, () => { postId: productCatalogTemplateId, } ); - await ensureSidebarOpened(); + await waitForCanvas(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - - const [ filterButtonToggle ] = await page.$x( - block.selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); + await enableApplyFiltersButton(); await saveTemplate(); await goToShopPage(); @@ -309,14 +300,10 @@ describe( `${ block.name } Block`, () => { it( 'should refresh the page only if the user clicks on button', async () => { await page.goto( editorPageUrl ); - await ensureSidebarOpened(); - await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - const [ filterButtonToggle ] = await page.$x( - block.selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); + await waitForCanvas(); + await selectBlockByName( block.slug ); + await enableApplyFiltersButton(); await saveOrPublish(); await page.goto( frontedPageUrl ); diff --git a/tests/e2e/specs/shopper/filter-products-by-price.test.ts b/tests/e2e/specs/shopper/filter-products-by-price.test.ts index cd5ca7c68e4..c9a68fccf2f 100644 --- a/tests/e2e/specs/shopper/filter-products-by-price.test.ts +++ b/tests/e2e/specs/shopper/filter-products-by-price.test.ts @@ -7,22 +7,20 @@ import { insertBlock, switchUserToAdmin, publishPost, - ensureSidebarOpened, } from '@wordpress/e2e-test-utils'; -import { - selectBlockByName, - insertBlockUsingSlash, - switchBlockInspectorTabWhenGutenbergIsInstalled, -} from '@woocommerce/blocks-test-utils'; +import { selectBlockByName } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ import { BASE_URL, + enableApplyFiltersButton, goToTemplateEditor, + insertAllProductsBlock, saveTemplate, useTheme, + waitForCanvas, waitForAllProductsBlockLoaded, } from '../../utils'; import { clickLink, saveOrPublish } from '../../../utils'; @@ -32,10 +30,6 @@ const block = { slug: 'woocommerce/price-filter', class: '.wc-block-price-filter', selectors: { - editor: { - filterButtonToggle: - '//label[text()="Show \'Apply filters\' button"]', - }, frontend: { priceMaxAmount: '.wc-block-price-filter__amount--max', productsList: '.wc-block-grid__products > li', @@ -75,7 +69,7 @@ describe( `${ block.name } Block`, () => { } ); await insertBlock( block.name ); - await insertBlockUsingSlash( 'All Products' ); + await insertAllProductsBlock(); await insertBlock( 'Active Filters' ); await publishPost(); @@ -186,17 +180,9 @@ describe( `${ block.name } Block`, () => { postId: productCatalogTemplateId, } ); + await waitForCanvas(); await selectBlockByName( block.slug ); - await ensureSidebarOpened(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - - await page.waitForXPath( - block.selectors.editor.filterButtonToggle - ); - const [ filterButtonToggle ] = await page.$x( - block.selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); + await enableApplyFiltersButton(); await saveTemplate(); await goToShopPage(); @@ -298,17 +284,9 @@ describe( `${ block.name } Block`, () => { it( 'should refresh the page only if the user click on button', async () => { await page.goto( editorPageUrl ); - await ensureSidebarOpened(); + await waitForCanvas(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - await page.waitForXPath( - block.selectors.editor.filterButtonToggle - ); - const [ filterButtonToggle ] = await page.$x( - block.selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); - + await enableApplyFiltersButton(); await saveOrPublish(); await page.goto( frontedPageUrl ); diff --git a/tests/e2e/specs/shopper/filter-products-by-rating.test.ts b/tests/e2e/specs/shopper/filter-products-by-rating.test.ts index 80a0a02151b..fe43f77985f 100644 --- a/tests/e2e/specs/shopper/filter-products-by-rating.test.ts +++ b/tests/e2e/specs/shopper/filter-products-by-rating.test.ts @@ -5,16 +5,15 @@ import { createNewPost, deleteAllTemplates, insertBlock, + switchBlockInspectorTab, switchUserToAdmin, publishPost, ensureSidebarOpened, } from '@wordpress/e2e-test-utils'; import { selectBlockByName, - insertBlockUsingSlash, saveOrPublish, getToggleIdByLabel, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { setCheckbox } from '@woocommerce/e2e-utils'; @@ -23,7 +22,9 @@ import { setCheckbox } from '@woocommerce/e2e-utils'; */ import { BASE_URL, + enableApplyFiltersButton, goToTemplateEditor, + insertAllProductsBlock, saveTemplate, useTheme, waitForAllProductsBlockLoaded, @@ -35,10 +36,6 @@ const block = { slug: 'woocommerce/rating-filter', class: '.wc-block-rating-filter', selectors: { - editor: { - filterButtonToggle: - '//label[text()="Show \'Apply filters\' button"]', - }, frontend: { productsList: '.wc-block-grid__products > li', queryProductsList: '.wp-block-post-template > li', @@ -70,7 +67,7 @@ describe( `${ block.name } Block`, () => { } ); await insertBlock( block.name ); - await insertBlockUsingSlash( 'All Products' ); + await insertAllProductsBlock(); await publishPost(); link = await page.evaluate( () => @@ -162,17 +159,7 @@ describe( `${ block.name } Block`, () => { await waitForCanvas(); await selectBlockByName( block.slug ); - await ensureSidebarOpened(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - - await page.waitForXPath( - block.selectors.editor.filterButtonToggle - ); - - const [ filterButtonToggle ] = await page.$x( - selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); + await enableApplyFiltersButton(); await saveTemplate(); await goToShopPage(); @@ -271,7 +258,7 @@ describe( `${ block.name } Block`, () => { await waitForCanvas(); await ensureSidebarOpened(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await switchBlockInspectorTab( 'Settings' ); await setCheckbox( await getToggleIdByLabel( "Show 'Apply filters' button", 1 ) ); diff --git a/tests/e2e/specs/shopper/filter-products-by-stock.test.ts b/tests/e2e/specs/shopper/filter-products-by-stock.test.ts index 7fdacc9a793..61283a76584 100644 --- a/tests/e2e/specs/shopper/filter-products-by-stock.test.ts +++ b/tests/e2e/specs/shopper/filter-products-by-stock.test.ts @@ -5,16 +5,15 @@ import { createNewPost, deleteAllTemplates, insertBlock, + switchBlockInspectorTab, switchUserToAdmin, publishPost, ensureSidebarOpened, } from '@wordpress/e2e-test-utils'; import { selectBlockByName, - insertBlockUsingSlash, getToggleIdByLabel, saveOrPublish, - switchBlockInspectorTabWhenGutenbergIsInstalled, } from '@woocommerce/blocks-test-utils'; import { setCheckbox } from '@woocommerce/e2e-utils'; @@ -23,7 +22,9 @@ import { setCheckbox } from '@woocommerce/e2e-utils'; */ import { BASE_URL, + enableApplyFiltersButton, goToTemplateEditor, + insertAllProductsBlock, saveTemplate, useTheme, waitForAllProductsBlockLoaded, @@ -35,10 +36,6 @@ const block = { slug: 'woocommerce/stock-filter', class: '.wc-block-stock-filter', selectors: { - editor: { - filterButtonToggle: - '//label[text()="Show \'Apply filters\' button"]', - }, frontend: { productsList: '.wc-block-grid__products > li', classicProductsList: '.products.columns-3 > li', @@ -69,7 +66,7 @@ describe( `${ block.name } Block`, () => { } ); await insertBlock( block.name ); - await insertBlockUsingSlash( 'All Products' ); + await insertAllProductsBlock(); await publishPost(); link = await page.evaluate( () => @@ -168,16 +165,7 @@ describe( `${ block.name } Block`, () => { await waitForCanvas(); await selectBlockByName( block.slug ); - await ensureSidebarOpened(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); - await page.waitForXPath( - block.selectors.editor.filterButtonToggle - ); - - const [ filterButtonToggle ] = await page.$x( - selectors.editor.filterButtonToggle - ); - await filterButtonToggle.click(); + await enableApplyFiltersButton(); await saveTemplate(); await goToShopPage(); @@ -276,7 +264,7 @@ describe( `${ block.name } Block`, () => { await waitForCanvas(); await ensureSidebarOpened(); await selectBlockByName( block.slug ); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); + await switchBlockInspectorTab( 'Settings' ); await setCheckbox( await getToggleIdByLabel( "Show 'Apply filters' button" ) ); diff --git a/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts b/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts index 9328004f709..bfd948485f3 100644 --- a/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts +++ b/tests/e2e/specs/shopper/product-query/product-query-with-templates.test.ts @@ -6,8 +6,6 @@ import { ensureSidebarOpened, } from '@wordpress/e2e-test-utils'; -import { switchBlockInspectorTabWhenGutenbergIsInstalled } from '@woocommerce/blocks-test-utils'; - /** * Internal dependencies */ @@ -20,7 +18,6 @@ import { import { addProductQueryBlock, block, - configureProductQueryBlock, getProductsNameFromClassicTemplate, getProductsNameFromProductQuery, toggleInheritQueryFromTemplateSetting, @@ -39,7 +36,6 @@ describe( `${ block.name } Block`, () => { } ); await ensureSidebarOpened(); await addProductQueryBlock(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); } ); it( 'when Inherit Query from template is disabled all the settings that customize the query should be hidden', async () => { @@ -55,7 +51,7 @@ describe( `${ block.name } Block`, () => { } ); it( 'when Inherit Query from template is enabled all the settings that customize the query should be hidden', async () => { - await configureProductQueryBlock(); + await ensureSidebarOpened(); const popularFilterEl = await page.$( block.selectors.editor.popularFilter @@ -78,7 +74,7 @@ describe( `${ block.name } Block`, () => { postId: productCatalogTemplateId, } ); await addProductQueryBlock(); - await configureProductQueryBlock(); + await ensureSidebarOpened(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -107,7 +103,7 @@ describe( `${ block.name } Block`, () => { postId: taxonomyProductCategory, } ); await addProductQueryBlock(); - await configureProductQueryBlock(); + await ensureSidebarOpened(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -139,7 +135,7 @@ describe( `${ block.name } Block`, () => { postId: tagProductCategory, } ); await addProductQueryBlock(); - await configureProductQueryBlock(); + await ensureSidebarOpened(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); @@ -171,7 +167,7 @@ describe( `${ block.name } Block`, () => { postId: productSearchResults, } ); await addProductQueryBlock(); - await configureProductQueryBlock(); + await ensureSidebarOpened(); await page.waitForNetworkIdle(); await saveTemplate(); await page.waitForNetworkIdle(); diff --git a/tests/e2e/specs/shopper/product-query/utils.ts b/tests/e2e/specs/shopper/product-query/utils.ts index 08c86c01b81..da8e3f2b30b 100644 --- a/tests/e2e/specs/shopper/product-query/utils.ts +++ b/tests/e2e/specs/shopper/product-query/utils.ts @@ -1,8 +1,7 @@ /** * External dependencies */ -import { insertBlock, ensureSidebarOpened } from '@wordpress/e2e-test-utils'; -import { switchBlockInspectorTabWhenGutenbergIsInstalled } from '@woocommerce/blocks-test-utils'; +import { insertBlock } from '@wordpress/e2e-test-utils'; /** * Internal dependencies @@ -39,11 +38,6 @@ export const toggleInheritQueryFromTemplateSetting = async () => { await button.click(); }; -export const configureProductQueryBlock = async () => { - await ensureSidebarOpened(); - await switchBlockInspectorTabWhenGutenbergIsInstalled( 'Settings' ); -}; - export const getProductsNameFromClassicTemplate = async () => { const products = await page.$$( block.selectors.frontend.classicProductsListName diff --git a/tests/e2e/utils.js b/tests/e2e/utils.js index 9184b226cb3..cc36c9b1525 100644 --- a/tests/e2e/utils.js +++ b/tests/e2e/utils.js @@ -4,13 +4,17 @@ import { Coupon, HTTPClientFactory } from '@woocommerce/api'; import config from 'config'; import { + canvas, disableSiteEditorWelcomeGuide, + ensureSidebarOpened, openGlobalBlockInserter, + switchBlockInspectorTab, switchUserToAdmin, visitAdminPage, pressKeyWithModifier, searchForBlock as searchForFSEBlock, enterEditMode, + getAllBlocks, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; import { WP_ADMIN_DASHBOARD } from '@woocommerce/e2e-utils'; @@ -69,6 +73,13 @@ const SELECTORS = { editButton: '.edit-site-site-hub__edit-button[aria-label="Open the editor"]', }, + editor: { + filterButtonToggle: '//label[text()="Show \'Apply filters\' button"]', + }, + frontend: { + XPathSubmitButton: + "//*[contains(@class,'wc-block-components-filter-submit-button')]", + }, }; /** @@ -149,20 +160,17 @@ export const isBlockInsertedInWidgetsArea = async ( blockName ) => { }; /** - * Visits site editor dependening on used WordPress version and how Gutenberg is installed. + * Visits site editor depending on used WordPress version and how Gutenberg is installed. * * @param {Object} params Query parameters to add to the URL. * @param {string} [params.postId] ID of the template if we want to access template editor. * @param {'wp_template' | 'wp_template_part'} [params.postType='wp_template'] Type of template. + * @param {string} [params.path] Navigation path. */ export async function goToSiteEditor( params = {} ) { await visitAdminPage( 'site-editor.php', addQueryArgs( '', params ) ); - // @todo Remove the Gutenberg guard clause in goToSiteEditor when WP 6.2 is released. - if ( - GUTENBERG_EDITOR_CONTEXT === 'gutenberg' && - ( params?.postId || Object.keys( params ).length === 0 ) - ) { + if ( params?.postId || Object.keys( params ).length === 0 ) { await enterEditMode(); } } @@ -198,7 +206,12 @@ export async function goToTemplatesList( { postType = 'wp_template', waitFor = 'list', } = {} ) { - await goToSiteEditor( { postType } ); + await goToSiteEditor( { + postType, + // In WP 6.2, if postId is not defined, the route expects `path` instead + // of `postType`. + path: `/${ postType }/all`, + } ); if ( waitFor === 'actions' ) { await page.waitForSelector( @@ -441,3 +454,97 @@ export const waitForAllProductsBlockLoaded = async () => { */ export const describeOrSkip = ( condition ) => condition ? describe : describe.skip; + +/** + * Get all blocks in the document that match a certain slug. + * + * @param {string} slug Slug of the blocks to get. + * + * @return {Promise<{}>} Promise resolving with an array containing all blocks in + * the document that match a certain slug. + */ +export const getBlocksBySlug = async ( slug ) => { + const blocks = await getAllBlocks(); + return blocks.filter( ( { name } ) => name === slug ); +}; + +/** + * Insert the All Products block using the global inserter. This util is needed + * because inserting the All Products block using the `insertBlock()` util + * causes time outs. + */ +export const insertAllProductsBlock = async () => { + const searchTerm = 'All Products'; + + await searchForBlock( searchTerm ); + + // Wait for the default block list to disappear to prevent its items from + // being considered as search results. This is needed since we're debouncing + // search request. + await page.waitForSelector( '.block-editor-inserter__block-list', { + hidden: true, + } ); + + const insertButton = await page.waitForXPath( + `//*[@role='option' and contains(., '${ searchTerm }')]` + ); + if ( ! insertButton ) { + throw new Error( `Could not find the "${ searchTerm }" block` ); + } + insertButton?.click(); +}; + +/** + * Clicks on the button in the header which opens Document Settings sidebar when it is closed. + * Based on https://github.com/WordPress/gutenberg/blob/trunk/packages/e2e-test-utils/src/open-document-settings-sidebar.js, + * but updates the selector so it works in WP 6.2 without GB. + */ +export async function openSettingsSidebar() { + const toggleButton = await page.waitForSelector( + '.edit-post-header__settings button[aria-label="Settings"]' + ); + + const isClosed = await page.evaluate( + ( element ) => element.getAttribute( 'aria-expanded' ) === 'false', + toggleButton + ); + + if ( isClosed ) { + await toggleButton.click(); + await page.waitForSelector( '.edit-post-sidebar' ); + } +} + +/** + * Enables the `Show 'Apply filters' button` toggle. + */ +export const enableApplyFiltersButton = async () => { + await ensureSidebarOpened(); + await switchBlockInspectorTab( 'Settings' ); + + await page.waitForXPath( SELECTORS.editor.filterButtonToggle ); + + const [ filterButtonToggle ] = await page.$x( + SELECTORS.editor.filterButtonToggle + ); + if ( ! filterButtonToggle ) { + throw new Error( "'Apply filters' toggle not found via XPath." ); + } + await filterButtonToggle.click(); + + // If for some reason click didn't work (it seems to happen intermittently), + // click on the toggle via JS. + await page.evaluate( () => { + const toggle = document.querySelector( + '.components-toggle-control:last-child .components-form-toggle__input' + ); + if ( ! toggle ) { + throw new Error( "'Apply filters' toggle not found via CSS." ); + } + if ( ! toggle.checked ) { + toggle.click(); + } + } ); + + await canvas().waitForXPath( SELECTORS.frontend.XPathSubmitButton ); +}; diff --git a/tests/utils/index.js b/tests/utils/index.js index 5093bacb05d..615bd8b98db 100644 --- a/tests/utils/index.js +++ b/tests/utils/index.js @@ -22,6 +22,4 @@ export { findToolsPanelWithTitle } from './find-tools-panel-with-title'; export { getFormElementIdByLabel } from './get-form-element-id-by-label'; export { getToggleIdByLabel } from './get-toggle-id-by-label'; export { insertBlockUsingQuickInserter } from './insert-block-using-quick-inserter'; -export { insertBlockUsingSlash } from './insert-block-using-slash'; export { insertShortcodeBlock } from './insert-shortcode-block'; -export { switchBlockInspectorTabWhenGutenbergIsInstalled } from './switch-block-inspector-tab-when-gutenberg-is-installed'; diff --git a/tests/utils/insert-block-using-slash.ts b/tests/utils/insert-block-using-slash.ts deleted file mode 100644 index 11ce7b95fa7..00000000000 --- a/tests/utils/insert-block-using-slash.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * External dependencies - */ -import { canvas, insertBlock } from '@wordpress/e2e-test-utils'; - -/** - * Internal dependencies - */ -import SELECTORS from './selectors'; - -export const insertBlockUsingSlash = async ( blockTitle: string ) => { - await insertBlock( 'Paragraph' ); - await canvas().keyboard.type( `/${ blockTitle }` ); - await canvas().waitForSelector( SELECTORS.popover ); - await canvas().keyboard.press( 'Enter' ); -}; diff --git a/tests/utils/shopper.js b/tests/utils/shopper.js index e1b969dce33..99855e58b3d 100644 --- a/tests/utils/shopper.js +++ b/tests/utils/shopper.js @@ -290,13 +290,18 @@ export const shopper = { await expect( page ).toFill( '#billing-address_1', customerBillingDetails.addressfirstline ); await expect( page ).toFill( '#billing-address_2', customerBillingDetails.addresssecondline ); await expect( page ).toFill( '#billing-city', customerBillingDetails.city ); - await expect( page ).toFill( '#billing-state input', customerBillingDetails.state ); + + const stateInputField = await page.$( '#billing-state input' ); + if ( stateInputField ) { + await expect( page ).toFill( '#billing-state input', customerBillingDetails.state ); + } await expect( page ).toFill( '#billing-postcode', customerBillingDetails.postcode ); await expect( page ).toFill( '#billing-phone', customerBillingDetails.phone ); await expect( page ).toFill( '#email', customerBillingDetails.email ); // Blur active field to trigger customer address update, then wait for requests to finish. await page.evaluate( 'document.activeElement.blur()' ); await checkCustomerPushCompleted( 'billing', customerBillingDetails ); + }, // prettier-ignore @@ -313,7 +318,10 @@ export const shopper = { await expect( page ).toFill( '#shipping-address_1', customerShippingDetails.addressfirstline ); await expect( page ).toFill( '#shipping-address_2', customerShippingDetails.addresssecondline ); await expect( page ).toFill( '#shipping-city', customerShippingDetails.city ); - await expect( page ).toFill( '#shipping-state input', customerShippingDetails.state ); + const stateInputField = await page.$( '#shipping-state input' ); + if ( stateInputField ) { + await expect( page ).toFill( '#shipping-state input', customerShippingDetails.state ); + } await expect( page ).toFill( '#shipping-postcode', customerShippingDetails.postcode ); await expect( page ).toFill( '#shipping-phone', customerShippingDetails.phone ); // Blur active field to customer address update, then wait for requests to finish. diff --git a/tests/utils/switch-block-inspector-tab-when-gutenberg-is-installed.ts b/tests/utils/switch-block-inspector-tab-when-gutenberg-is-installed.ts deleted file mode 100644 index ba399795e85..00000000000 --- a/tests/utils/switch-block-inspector-tab-when-gutenberg-is-installed.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * External dependencies - */ -import { switchBlockInspectorTab } from '@wordpress/e2e-test-utils'; - -/** - * Internal dependencies - */ -import { GUTENBERG_EDITOR_CONTEXT } from '../e2e/utils'; - -// @todo Remove this function when WP 6.2 is released. We can use the "switchBlockInspectorTab" function directly. -export const switchBlockInspectorTabWhenGutenbergIsInstalled = async ( - tabName: string -) => { - if ( GUTENBERG_EDITOR_CONTEXT === 'core' ) { - return; - } - await switchBlockInspectorTab( tabName ); -}; diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 1dcbb065f16..7d4b46be358 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Blocks * Plugin URI: https://github.com/woocommerce/woocommerce-gutenberg-products-block * Description: WooCommerce blocks for the Gutenberg editor. - * Version: 9.9.0-dev + * Version: 10.0.0-dev * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woo-gutenberg-products-block