From 26550194d345ce0a08e4dbbe624856faf267c38d Mon Sep 17 00:00:00 2001 From: Manish Menaria Date: Mon, 20 Nov 2023 18:48:59 +0530 Subject: [PATCH] Product Collection - New 'No Results' block with default UI (https://github.com/woocommerce/woocommerce-blocks/pull/11783) * Add custom 'No Results' block for Product Collection This commit introduces a new 'No Results' block, replacing the default core 'query-no-results' block within product collection block. The 'No Results' block provides a tailored experience for WooCommerce stores, displaying a custom message when no products are found in a query. Key changes include: - A new block type 'woocommerce/no-results' is registered with a complete configuration and content structure. - Edit and save functions are implemented for the block, allowing for custom content and styles within the block editor and on the front end. - Webpack entries are updated to include the new block in the build process. - A PHP class NoResults is added for server-side rendering, which only displays the block when the product query returns no results, enhancing performance. - The ProductCollectionUtils utility class is extended to support the new block's query needs. The new block enriches the user experience by providing clearer communication when no products match the collection criteria and allows store owners to customize the message and presentation. * Update description * Rename 'no-results' block to 'product-collection-no-results' Changes include: - Updating the block name in `edit.tsx` within the `product-collection` directory. - Modifying the block.json file in the `no-results` inner block to reflect the new name. - Adjusting the className in `edit.tsx` for the `no-results` inner block. - Altering the webpack entries in `webpack-entries.js` to recognize the new block name. - Renaming the block in `NoResults.php` to align with this update. This renaming aims to make the block's purpose more clear and to maintain a consistent naming scheme within our product collection blocks. * Rename NoResults to ProductCollectionNoResults for clarity This commit renames the `NoResults` class to `ProductCollectionNoResults`. The change aims to enhance clarity and specificity about the class's purpose, indicating that it specifically handles no-result scenarios within product collections. Changes made: - Renamed `NoResults.php` to `ProductCollectionNoResults.php`. - Updated the class name from `NoResults` to `ProductCollectionNoResults` in the file. - Modified the reference in `BlockTypesController.php` to use the new class name. This renaming ensures better readability and understanding of the class's role in the context of product collections. The primary change is the renaming, with no significant alterations in the class functionality. * Update No-Results Message Formatting in Product Collection Block This commit simplifies the layout and message content for the 'No results found' message in the product collection block's no-results edit component. The changes include: 1. Removal of the full stop in the 'No results found' string for consistency. 2. Replacing the 'core/group' block with a 'core/paragraph' block. 3. Streamlining the message content to be more concise and integrated into fewer text blocks. 4. Direct links for 'clearing any filters' and navigating to the 'store's home' are now included in the same paragraph. --------- Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com> --- .../js/blocks/product-collection/edit.tsx | 2 +- .../inner-blocks/no-results/block.json | 35 +++++ .../inner-blocks/no-results/edit.tsx | 65 ++++++++ .../inner-blocks/no-results/index.tsx | 21 +++ .../inner-blocks/no-results/save.tsx | 9 ++ .../woocommerce-blocks/bin/webpack-entries.js | 3 + .../BlockTypes/ProductCollectionNoResults.php | 141 ++++++++++++++++++ .../src/BlockTypes/ProductTemplate.php | 16 +- .../src/BlockTypesController.php | 1 + .../src/Utils/ProductCollectionUtils.php | 34 +++++ 10 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/block.json create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/edit.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/save.tsx create mode 100644 plugins/woocommerce-blocks/src/BlockTypes/ProductCollectionNoResults.php create mode 100644 plugins/woocommerce-blocks/src/Utils/ProductCollectionUtils.php diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit.tsx index dfc53b1e662..9da8bbc60a9 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit.tsx @@ -75,7 +75,7 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [ }, }, ], - [ 'core/query-no-results' ], + [ 'woocommerce/product-collection-no-results' ], ]; const Edit = ( props: BlockEditProps< ProductCollectionAttributes > ) => { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/block.json new file mode 100644 index 00000000000..c0912c011fa --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/block.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "woocommerce/product-collection-no-results", + "title": "No results", + "version": "1.0.0", + "category": "woocommerce", + "description": "The contents of this block will display when there are no products found.", + "textdomain": "woo-gutenberg-products-block", + "keywords": [ "Product Collection" ], + "usesContext": [ "queryId", "query" ], + "ancestor": [ "woocommerce/product-collection" ], + "supports": { + "align": true, + "reusable": false, + "html": false, + "color": { + "gradients": true, + "link": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/edit.tsx new file mode 100644 index 00000000000..ee0533f5a96 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/edit.tsx @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; +import { Template } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; + +const TEMPLATE: Template[] = [ + [ + 'core/group', + { + layout: { + type: 'flex', + orientation: 'vertical', + justifyContent: 'center', + flexWrap: 'wrap', + }, + }, + [ + [ + 'core/paragraph', + { + textAlign: 'center', + fontSize: 'medium', + content: `${ __( + 'No results found', + 'woo-gutenberg-products-block' + ) }`, + }, + ], + [ + 'core/paragraph', + { + content: `${ __( + 'You can try', + 'woo-gutenberg-products-block' + ) } ${ __( + 'clearing any filters', + 'woo-gutenberg-products-block' + ) } ${ __( + 'or head to our', + 'woo-gutenberg-products-block' + ) } ${ __( + "store's home", + 'woo-gutenberg-products-block' + ) }`, + }, + ], + ], + ], +]; + +const Edit = () => { + const blockProps = useBlockProps( { + className: 'wc-block-product-collection-no-results', + } ); + + return ( +
+ +
+ ); +}; + +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/index.tsx new file mode 100644 index 00000000000..22da03fb4b8 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/index.tsx @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import { loop as loopIcon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +registerBlockType( metadata, { + icon: loopIcon, + supports: { + ...metadata.supports, + }, + edit, + save, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/save.tsx new file mode 100644 index 00000000000..3064a7b9d6a --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inner-blocks/no-results/save.tsx @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { InnerBlocks } from '@wordpress/block-editor'; + +export default function NoResultsSave() { + // @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core + return ; +} diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index f037be12806..6e23e4beb52 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -50,6 +50,9 @@ const blocks = { 'product-category': {}, 'product-categories': {}, 'product-collection': {}, + 'product-collection-no-results': { + customDir: 'product-collection/inner-blocks/no-results', + }, 'product-gallery': { isExperimental: true, }, diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductCollectionNoResults.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductCollectionNoResults.php new file mode 100644 index 00000000000..9d9fdeb0015 --- /dev/null +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductCollectionNoResults.php @@ -0,0 +1,141 @@ +post_count > 0 ) { + return ''; + } + + // Update the anchor tag URLs. + $updated_html_content = $this->modify_anchor_tag_urls( trim( $content ) ); + + $wrapper_attributes = get_block_wrapper_attributes(); + return sprintf( + '
%2$s
', + $wrapper_attributes, + $updated_html_content + ); + } + + /** + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + */ + protected function get_block_type_script( $key = null ) { + return null; + } + + /** + * Set the URL attributes for "clearing any filters" and "Store's home" links. + * + * @param string $content Block content. + */ + protected function modify_anchor_tag_urls( $content ) { + $processor = new \WP_HTML_Tag_Processor( trim( $content ) ); + + // Set the URL attribute for the "clear any filters" link. + if ( $processor->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wc-link-clear-any-filters', + ) + ) ) { + $processor->set_attribute( 'href', $this->get_current_url_without_filters() ); + } + + // Set the URL attribute for the "Store's home" link. + if ( $processor->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wc-link-stores-home', + ) + ) ) { + $processor->set_attribute( 'href', home_url() ); + } + + return $processor->get_updated_html(); + } + + /** + * Get current URL without filter query parameters which will be used + * for the "clear any filters" link. + */ + protected function get_current_url_without_filters() { + $protocol = is_ssl() ? 'https' : 'http'; + + // Check the existence and sanitize HTTP_HOST and REQUEST_URI in the $_SERVER superglobal. + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $http_host = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : ''; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; + + // Sanitize the host and URI. + $http_host = sanitize_text_field( $http_host ); + $request_uri = esc_url_raw( $request_uri ); + + // Construct the full URL. + $current_url = $protocol . '://' . $http_host . $request_uri; + + // Parse the URL to extract the query string. + $parsed_url = wp_parse_url( $current_url ); + $query_string = isset( $parsed_url['query'] ) ? $parsed_url['query'] : ''; + + // Convert the query string into an associative array. + parse_str( $query_string, $query_params ); + + // Remove the filter query parameters. + $params_to_remove = array( 'min_price', 'max_price', 'rating_filter', 'filter_', 'query_type_' ); + foreach ( $query_params as $key => $value ) { + foreach ( $params_to_remove as $param ) { + if ( strpos( $key, $param ) === 0 ) { + unset( $query_params[ $key ] ); + break; + } + } + } + + // Rebuild the query string without the removed parameters. + $new_query_string = http_build_query( $query_params ); + + // Reconstruct the URL. + $new_url = $parsed_url['scheme'] . '://' . $parsed_url['host']; + $new_url .= isset( $parsed_url['path'] ) ? $parsed_url['path'] : ''; + $new_url .= $new_query_string ? '?' . $new_query_string : ''; + + return $new_url; + } + +} diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductTemplate.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductTemplate.php index 7c7b06acd90..48ba2610c61 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductTemplate.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductTemplate.php @@ -2,8 +2,8 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes; +use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils; use WP_Block; -use WP_Query; /** * ProductTemplate class. @@ -36,19 +36,7 @@ class ProductTemplate extends AbstractBlock { * @return string | void Rendered block output. */ protected function render( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - // phpcs:ignore WordPress.Security.NonceVerification - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - - // Use global query if needed. - $use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ); - if ( $use_global_query ) { - global $wp_query; - $query = clone $wp_query; - } else { - $query_args = build_query_vars_from_query_block( $block, $page ); - $query = new WP_Query( $query_args ); - } + $query = ProductCollectionUtils::prepare_and_execute_query( $block ); if ( ! $query->have_posts() ) { return ''; diff --git a/plugins/woocommerce-blocks/src/BlockTypesController.php b/plugins/woocommerce-blocks/src/BlockTypesController.php index 26fe7d3dd1b..57d2f1ad73a 100644 --- a/plugins/woocommerce-blocks/src/BlockTypesController.php +++ b/plugins/woocommerce-blocks/src/BlockTypesController.php @@ -239,6 +239,7 @@ final class BlockTypesController { 'ProductCategories', 'ProductCategory', 'ProductCollection', + 'ProductCollectionNoResults', 'ProductImage', 'ProductImageGallery', 'ProductNew', diff --git a/plugins/woocommerce-blocks/src/Utils/ProductCollectionUtils.php b/plugins/woocommerce-blocks/src/Utils/ProductCollectionUtils.php new file mode 100644 index 00000000000..9f77d863e77 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Utils/ProductCollectionUtils.php @@ -0,0 +1,34 @@ +context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + // phpcs:ignore WordPress.Security.NonceVerification + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + + // Use global query if needed. + $use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ); + if ( $use_global_query ) { + global $wp_query; + $query = clone $wp_query; + } else { + $query_args = build_query_vars_from_query_block( $block, $page ); + $query = new WP_Query( $query_args ); + } + + return $query; + } +}