diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.tsx similarity index 63% rename from plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.js rename to plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.tsx index 1309a63feb8..b64ad521256 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews/frontend-container-block.tsx @@ -4,51 +4,71 @@ import { __, _n, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import { Component } from '@wordpress/element'; -import PropTypes from 'prop-types'; +import { Review } from '@woocommerce/base-components/reviews/types'; /** * Internal dependencies */ import { getSortArgs } from './utils'; import FrontendBlock from './frontend-block'; +import { ReviewBlockAttributes } from './attributes'; + +type FrontendContainerBlockProps = { + attributes: ReviewBlockAttributes; +}; /** * Container of the block rendered in the frontend. */ -class FrontendContainerBlock extends Component { - constructor() { - super( ...arguments ); +class FrontendContainerBlock extends Component< + FrontendContainerBlockProps, + { orderby: string; reviewsToDisplay: number } +> { + constructor( props: FrontendContainerBlockProps ) { + super( props ); const { attributes } = this.props; this.state = { - orderby: attributes.orderby, - reviewsToDisplay: parseInt( attributes.reviewsOnPageLoad, 10 ), + orderby: attributes?.orderby, + reviewsToDisplay: this.getReviewsOnPageLoad(), }; this.onAppendReviews = this.onAppendReviews.bind( this ); this.onChangeOrderby = this.onChangeOrderby.bind( this ); } - onAppendReviews() { + getReviewsOnPageLoad() { const { attributes } = this.props; + + return typeof attributes.reviewsOnPageLoad === 'number' + ? attributes.reviewsOnPageLoad + : parseInt( attributes.reviewsOnPageLoad, 10 ); + } + + getReviewsOnLoadMore() { + const { attributes } = this.props; + + return typeof attributes.reviewsOnLoadMore === 'number' + ? attributes.reviewsOnLoadMore + : parseInt( attributes.reviewsOnLoadMore, 10 ); + } + + onAppendReviews() { const { reviewsToDisplay } = this.state; this.setState( { - reviewsToDisplay: - reviewsToDisplay + parseInt( attributes.reviewsOnLoadMore, 10 ), + reviewsToDisplay: reviewsToDisplay + this.getReviewsOnLoadMore(), } ); } - onChangeOrderby( event ) { - const { attributes } = this.props; - + onChangeOrderby( event: React.ChangeEvent< HTMLSelectElement > ) { this.setState( { orderby: event.target.value, - reviewsToDisplay: parseInt( attributes.reviewsOnPageLoad, 10 ), + reviewsToDisplay: this.getReviewsOnPageLoad(), } ); } - onReviewsAppended( { newReviews } ) { + onReviewsAppended( { newReviews }: { newReviews: Review[] } ) { speak( sprintf( /* translators: %d is the count of reviews loaded. */ @@ -83,6 +103,7 @@ class FrontendContainerBlock extends Component { const { order, orderby } = getSortArgs( this.state.orderby ); return ( + // @ts-expect-error - TODO: Refactor WrappedComponent { +const getProps = ( el: HTMLElement ) => { const showOrderby = el.dataset.showOrderby === 'true'; const showLoadMore = el.dataset.showLoadMore === 'true'; @@ -32,6 +32,4 @@ const getProps = ( el ) => { }; }; -// @ts-ignore -// Current typing does not work with non-functional components renderFrontend( { selector, Block: FrontendContainerBlock, getProps } ); diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index 6ffa30dc166..6b4a12abd48 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -60,6 +60,7 @@ const blocks = { 'product-gallery-large-image-next-previous': { customDir: 'product-gallery/inner-blocks/product-gallery-large-image-next-previous', + isExperimental: true, }, 'product-gallery-pager': { customDir: 'product-gallery/inner-blocks/product-gallery-pager', @@ -166,7 +167,7 @@ const entries = { ...getBlockEntries( 'index.{t,j}s{,x}' ), }, frontend: { - reviews: './assets/js/blocks/reviews/frontend.js', + reviews: './assets/js/blocks/reviews/frontend.ts', ...getBlockEntries( 'frontend.{t,j}s{,x}' ), 'mini-cart-component': './assets/js/blocks/mini-cart/component-frontend.tsx', diff --git a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md index 9b34c29b92d..f2e32c0c8bb 100644 --- a/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md +++ b/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md @@ -34,11 +34,11 @@ The majority of our feature flagging is blocks, this is a list of them: ### Experimental flag -- Product Gallery ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/src/BlockTypesController.php#L232) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/bin/webpack-entries.js#L50-L52C3) | [BlockTemplatesController](https://github.com/woocommerce/woocommerce-blocks/blob/211960f753d093f2f819273e130b34f893a784cd/src/BlockTemplatesController.php/#L467-L469)). -- Product Gallery Thumbnails ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/04af396b9aec5a915ad98188eded53e723a051d3/src/BlockTypesController.php#L234) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/04af396b9aec5a915ad98188eded53e723a051d3/bin/webpack-entries.js#L57-L60)). -- Product Average Rating ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/1111e2fb9d6f5074df96a444b99e2fc00e4eb8d1/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/1111e2fb9d6f5074df96a444b99e2fc00e4eb8d1/bin/webpack-entries.js#L68-L70)) -- Product Rating Stars ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L230) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L68-L70)) -- Product Rating Counter ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L71-L73)) +- Product Gallery ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/src/BlockTypesController.php#L234) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/bin/webpack-entries.js#L53-L55) | [BlockTemplatesController](https://github.com/woocommerce/woocommerce-blocks/blob/211960f753d093f2f819273e130b34f893a784cd/src/BlockTemplatesController.php/#L467-L469)). +- Product Gallery Large Image ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/src/BlockTypesController.php#L235) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/bin/webpack-entries.js#L56-L59)). +- Product Gallery Next/Previous Buttons ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/src/BlockTypesController.php#L236) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/bin/webpack-entries.js#L60-L63)). +- Product Gallery Pager ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/src/BlockTypesController.php#L237) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/bin/webpack-entries.js#L64-L67)). +- Product Gallery Thumbnails ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/src/BlockTypesController.php#L238) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/7f0d55d54885f436778f04a6389e92b8785d5c68/bin/webpack-entries.js#L68-L71)). - ⚛️ Add to cart ([JS flag](https://github.com/woocommerce/woocommerce-blocks/blob/dfd2902bd8a247b5d048577db6753c5e901fc60f/assets/js/atomic/blocks/product-elements/add-to-cart/index.ts#L26-L29)). - Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/b4a9dc9334f82c09f533b0f88c947b5c34e4e546/src/StoreApi/RoutesController.php#L65-L67)) - Checkout Order Route ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/b4ba06a6242cbb6a64d2ec554a263ebe60d8d3af/src/StoreApi/RoutesController.php#L67)) diff --git a/plugins/woocommerce-blocks/src/BlockTemplatesController.php b/plugins/woocommerce-blocks/src/BlockTemplatesController.php index 52f09f48db3..0261ff5c225 100644 --- a/plugins/woocommerce-blocks/src/BlockTemplatesController.php +++ b/plugins/woocommerce-blocks/src/BlockTemplatesController.php @@ -10,6 +10,7 @@ use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate; use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils; use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate; +use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils; /** @@ -429,8 +430,12 @@ class BlockTemplatesController { } } - $new_content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content ); - $template->content = $new_content; + if ( post_password_required() ) { + $template->content = SingleProductTemplate::add_password_form( $template->content ); + } else { + $new_content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content ); + $template->content = $new_content; + } } } diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductGalleryThumbnails.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductGalleryThumbnails.php index 6739f7466e1..252ce5c36e9 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductGalleryThumbnails.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductGalleryThumbnails.php @@ -5,7 +5,7 @@ use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils; use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils; /** - * ProductGalleryLargeImage class. + * ProductGalleryThumbnails class. */ class ProductGalleryThumbnails extends AbstractBlock { /** @@ -49,15 +49,14 @@ class ProductGalleryThumbnails extends AbstractBlock { $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); - $post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : ''; - $product = wc_get_product( $post_id ); - $post_thumbnail_id = $product->get_image_id(); - $html = ''; + $post_id = $block->context['postId'] ?? ''; + $product = wc_get_product( $post_id ); if ( $product ) { $post_thumbnail_id = $product->get_image_id(); $product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() ); if ( $product_gallery_images && $post_thumbnail_id ) { + $html = ''; $number_of_thumbnails = isset( $block->context['thumbnailsNumberOfThumbnails'] ) ? $block->context['thumbnailsNumberOfThumbnails'] : 3; $thumbnails_count = 1; @@ -83,17 +82,18 @@ class ProductGalleryThumbnails extends AbstractBlock { $thumbnails_count++; } - } - return sprintf( - '', - esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classes_and_styles['styles'] ), - $html - ); + return sprintf( + '', + esc_attr( $classes_and_styles['classes'] ), + esc_attr( $classes_and_styles['styles'] ), + $html + ); + } } + return; } } } diff --git a/plugins/woocommerce-blocks/src/Templates/SingleProductTemplate.php b/plugins/woocommerce-blocks/src/Templates/SingleProductTemplate.php new file mode 100644 index 00000000000..a03d0d29783 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Templates/SingleProductTemplate.php @@ -0,0 +1,120 @@ + $carry['blocks'], + 'html_block' => null, + 'removed' => true, + 'is_already_replaced' => true, + + ); + } + + return array( + 'blocks' => $carry['blocks'], + 'html_block' => parse_blocks( '' . get_the_password_form() . '' )[0], + 'removed' => false, + 'is_already_replaced' => $carry['is_already_replaced'], + ); + + } + + if ( isset( $block['innerBlocks'] ) && count( $block['innerBlocks'] ) > 0 ) { + $index = 0; + $new_inner_blocks = array(); + $new_inner_contents = $block['innerContent']; + foreach ( $block['innerContent'] as $inner_content ) { + // Don't process the closing tag of the block. + if ( count( $block['innerBlocks'] ) === $index ) { + break; + } + + $blocks = self::replace_first_single_product_template_block_with_password_form( array( $block['innerBlocks'][ $index ] ), $carry['is_already_replaced'] ); + $new_blocks = $blocks['blocks']; + $html_block = $blocks['html_block']; + $is_removed = $blocks['removed']; + $carry['is_already_replaced'] = $blocks['is_already_replaced']; + + if ( isset( $html_block ) ) { + $new_inner_blocks = array_merge( $new_inner_blocks, $new_blocks, array( $html_block ) ); + $carry['is_already_replaced'] = true; + } else { + $new_inner_blocks = array_merge( $new_inner_blocks, $new_blocks ); + } + + if ( $is_removed ) { + unset( $new_inner_contents[ $index ] ); + // The last element of the inner contents contains the closing tag of the block. We don't want to remove it. + if ( $index + 1 < count( $new_inner_contents ) ) { + unset( $new_inner_contents[ $index + 1 ] ); + } + $new_inner_contents = array_values( $new_inner_contents ); + } + + $index++; + } + + $block['innerBlocks'] = $new_inner_blocks; + $block['innerContent'] = $new_inner_contents; + + return array( + 'blocks' => array_merge( $carry['blocks'], array( $block ) ), + 'html_block' => null, + 'removed' => false, + 'is_already_replaced' => $carry['is_already_replaced'], + ); + } + + return array( + 'blocks' => array_merge( $carry['blocks'], array( $block ) ), + 'html_block' => null, + 'removed' => false, + 'is_already_replaced' => $carry['is_already_replaced'], + ); + }, + array( + 'blocks' => array(), + 'html_block' => null, + 'removed' => false, + 'is_already_replaced' => $is_already_replaced, + ) + ); + } + + /** + * Add password form to the Single Product Template. + * + * @param string $content The content of the template. + * @return string + */ + public static function add_password_form( $content ) { + $parsed_blocks = parse_blocks( $content ); + $blocks = self::replace_first_single_product_template_block_with_password_form( $parsed_blocks, false ); + $serialized_blocks = serialize_blocks( $blocks['blocks'] ); + + return $serialized_blocks; + } +} diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/single-product-template/single-product-template-protected-product.block_theme.side_effects.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/single-product-template/single-product-template-protected-product.block_theme.side_effects.spec.ts new file mode 100644 index 00000000000..1ef1c5665dd --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/single-product-template/single-product-template-protected-product.block_theme.side_effects.spec.ts @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import { test, expect } from '@woocommerce/e2e-playwright-utils'; +import { cli } from '@woocommerce/e2e-utils'; + +const product = { + name: 'Protected Product', + slug: 'protected-product', + password: 'password', +}; +test.describe( 'Single Product Template', () => { + let id: null | string = null; + test.beforeEach( async ( { admin, page } ) => { + await admin.visitAdminPage( `/post-new.php?post_type=product` ); + + const input = page.locator( '#title' ); + await input.fill( product.name ); + await page.getByRole( 'button', { name: 'Edit visibility' } ).click(); + + await page.locator( '#visibility-radio-password' ).click(); + await page.locator( '#post_password' ).fill( product.password ); + await page.waitForResponse( ( response ) => + response.url().includes( 'admin-ajax.php' ) + ); + await page.locator( '#publish.button-primary' ).click(); + await page.waitForSelector( + '#woocommerce-product-updated-message-view-product__link' + ); + const url = new URL( page.url() ); + const queryParams = new URLSearchParams( url.search ); + id = queryParams.get( 'post' ); + } ); + + test.afterAll( async () => { + await cli( + `npm run wp-env run tests-cli -- wp post delete ${ id } --force` + ); + } ); + + test.describe( + `should render a password input when the product is protected `, + () => + test( 'add product specific classes to the body', async ( { + page, + } ) => { + await page.goto( `/product/${ product.slug }` ); + const placeholder = page.getByText( + 'This content is password protected. To view it please enter your password below:' + ); + + await expect( placeholder ).toBeVisible(); + + await page.getByLabel( 'Password' ).fill( 'password' ); + + await page.getByRole( 'button', { name: 'Enter' } ).click(); + + await expect( placeholder ).toBeHidden(); + } ) + ); +} ); diff --git a/plugins/woocommerce-blocks/tests/php/Templates/SingleProductTemplateTests.php b/plugins/woocommerce-blocks/tests/php/Templates/SingleProductTemplateTests.php new file mode 100644 index 00000000000..d1209f094b6 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Templates/SingleProductTemplateTests.php @@ -0,0 +1,247 @@ + + +
+ +
+ + '; + + $expected_single_product_template = ' + + +
+ +
+ + '; + + $result = SingleProductTemplate::add_password_form( + $default_single_product_template + ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $expected_single_product_template_without_whitespace = preg_replace( + '/\s+/', + '', + $expected_single_product_template + ); + + $this->assertEquals( + $result_without_withespace, + $expected_single_product_template_without_whitespace, + '' + ); + } + + /** + * Test that the password form is added to the Single Product Template. + */ + public function test_replace_single_product_blocks_with_input_form() { + $default_single_product_template = ' + + +
+ +
+ + '; + + $expected_single_product_template = sprintf( + ' + + +
+ %s +
+ + ', + get_the_password_form() + ); + + $result = SingleProductTemplate::add_password_form( + $default_single_product_template + ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $result_without_withespace_without_custom_pwbox_ids = preg_replace( + '/pwbox-\d+/', + '', + $result_without_withespace + ); + + $expected_single_product_template_without_whitespace = preg_replace( + '/\s+/', + '', + $expected_single_product_template + ); + + $expected_single_product_template_without_whitespace_without_custom_pwbox_ids = preg_replace( + '/pwbox-\d+/', + '', + $expected_single_product_template_without_whitespace + ); + + $this->assertEquals( + $result_without_withespace_without_custom_pwbox_ids, + $expected_single_product_template_without_whitespace_without_custom_pwbox_ids, + '' + ); + } + + /** + * Test that the password form is added to the Single Product Template with the default template. + */ + public function test_replace_default_template_single_product_blocks_with_input_form() { + $default_single_product_template = ' + + + +
+ + + + +
+ +
+ +
+ + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+ +
+ +
+ +
+ + + + + + + +
+ + + + + '; + + $expected_single_product_template = sprintf( + ' + + +
+ + + +
+ +
+ %s +
+ + +
+ + +
+ +
+ +
+ + ', + get_the_password_form() + ); + + $result = SingleProductTemplate::add_password_form( + $default_single_product_template + ); + + $result_without_withespace = preg_replace( '/\s+/', '', $result ); + $result_without_withespace_without_custom_pwbox_ids = preg_replace( + '/pwbox-\d+/', + '', + $result_without_withespace + ); + + $expected_single_product_template_without_whitespace = preg_replace( + '/\s+/', + '', + $expected_single_product_template + ); + + $expected_single_product_template_without_whitespace_without_custom_pwbox_ids = preg_replace( + '/pwbox-\d+/', + '', + $expected_single_product_template_without_whitespace + ); + + $this->assertEquals( + $result_without_withespace_without_custom_pwbox_ids, + $expected_single_product_template_without_whitespace_without_custom_pwbox_ids, + '' + ); + } +}