From 0625a1c170c77f7e6fea94a4f3891159f6ace889 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Tue, 16 Aug 2022 18:39:06 +0200 Subject: [PATCH 1/8] Move `EditorBlock` to general `type-defs` `EditorBlock` was scoped under the `featured-items` directory at the time of its creation. It is, however, a useful type that should be shared repo-wide. For this reason, I am moving it into the `blocks` type-defs and updating all the references. --- assets/js/types/type-defs/blocks.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/js/types/type-defs/blocks.ts b/assets/js/types/type-defs/blocks.ts index e7e7f13d662..aa6e6c0d844 100644 --- a/assets/js/types/type-defs/blocks.ts +++ b/assets/js/types/type-defs/blocks.ts @@ -1,8 +1,11 @@ /** * External dependencies */ +import type { BlockEditProps, BlockInstance } from '@wordpress/blocks'; import { LazyExoticComponent } from 'react'; +export type EditorBlock< T > = BlockInstance< T > & BlockEditProps< T >; + export type RegisteredBlockComponent = | LazyExoticComponent< React.ComponentType< unknown > > | ( () => JSX.Element | null ) From dd8ebe676347f8f6a90815c03ee0e0894c80be8d Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Tue, 16 Aug 2022 18:39:55 +0200 Subject: [PATCH 2/8] Define types for the Product Query block Also defines a more generic `WooCommerceBlockVariation` type which should be also useful in the future to implement a similar pattern. --- assets/js/blocks/product-query/types.ts | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 assets/js/blocks/product-query/types.ts diff --git a/assets/js/blocks/product-query/types.ts b/assets/js/blocks/product-query/types.ts new file mode 100644 index 00000000000..0675128f512 --- /dev/null +++ b/assets/js/blocks/product-query/types.ts @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { BlockInstance } from '@wordpress/blocks'; +import type { EditorBlock } from '@woocommerce/types'; + +export interface ProductQueryArguments { + /** + * Display only products on sale. + * + * Will generate the following `meta_query`: + * + * ``` + * array( + * 'relation' => 'OR', + * array( // Simple products type + * 'key' => '_sale_price', + * 'value' => 0, + * 'compare' => '>', + * 'type' => 'numeric', + * ), + * array( // Variable products type + * 'key' => '_min_variation_sale_price', + * 'value' => 0, + * 'compare' => '>', + * 'type' => 'numeric', + * ), + * ) + * ``` + */ + onSale?: boolean; +} + +export type ProductQueryBlock = + WooCommerceBlockVariation< ProductQueryAttributes >; + +export interface ProductQueryAttributes { + /** + * An array of controls to disable in the inspector. + * + * @example `[ 'stockStatus' ]` will not render the dropdown for stock status. + */ + disabledInspectorControls?: string[]; + /** + * Query attributes that define which products will be fetched. + */ + query?: ProductQueryArguments; +} + +export interface QueryBlockQuery { + author?: string; + exclude?: string[]; + inherit: boolean; + offset?: number; + order: 'asc' | 'desc'; + orderBy: 'date' | 'relevance'; + pages?: number; + parents?: number[]; + perPage?: number; + postType: string; + search?: string; + sticky?: string; + taxQuery?: string; +} + +export enum QueryVariation { + /** The main, fully customizable, Product Query block */ + PRODUCT_QUERY = 'product-query', + PRODUCTS_ON_SALE = 'query-on-sale', +} + +export type WooCommerceBlockVariation< T > = EditorBlock< { + // Disabling naming convention because we are namespacing our + // custom attributes inside a core block. Prefixing with underscores + // will help signify our intentions. + // eslint-disable-next-line @typescript-eslint/naming-convention + __woocommerceVariationProps: Partial< BlockInstance< T > >; +} >; From 936283c3b3df797f72721af1ab6f18697f178bcb Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Tue, 16 Aug 2022 18:40:36 +0200 Subject: [PATCH 3/8] Add Product Query utils Add two utility functions: 1. `isWooQueryBlockVariation`: is used to check whether a given block is a variation of the core Query Loop block, and also one of the allowed variations within our repo. See: `QueryVariation` enum type. 2. `setCustomQueryAttribute`: is a shorthand to set an attribute within the variation query attribute. --- assets/js/blocks/product-query/utils.tsx | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 assets/js/blocks/product-query/utils.tsx diff --git a/assets/js/blocks/product-query/utils.tsx b/assets/js/blocks/product-query/utils.tsx new file mode 100644 index 00000000000..61be85d5e38 --- /dev/null +++ b/assets/js/blocks/product-query/utils.tsx @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +import { + ProductQueryArguments, + ProductQueryBlock, + QueryVariation, +} from './types'; + +/** + * Identifies if a block is a Query block variation from our conventions + * + * We are extending Gutenberg's core Query block with our variations, and + * also adding extra namespaced attributes. If those namespaced attributes + * are present, we can be fairly sure it is our own registered variation. + */ +export function isWooQueryBlockVariation( block: ProductQueryBlock ) { + return ( + block.name === 'core/query' && + block.attributes.__woocommerceVariationProps && + Object.values( QueryVariation ).includes( + block.attributes.__woocommerceVariationProps + .name as unknown as QueryVariation + ) + ); +} + +/** + * Sets the new query arguments of a Product Query block + * + * Because we add a new set of deeply nested attributes to the query + * block, this utility function makes it easier to change just the + * options relating to our custom query, while keeping the code + * clean. + */ +export function setCustomQueryAttribute( + block: ProductQueryBlock, + attributes: Partial< ProductQueryArguments > +) { + const { __woocommerceVariationProps } = block.attributes; + + block.setAttributes( { + __woocommerceVariationProps: { + ...__woocommerceVariationProps, + attributes: { + ...__woocommerceVariationProps.attributes, + query: { + ...__woocommerceVariationProps.attributes?.query, + ...attributes, + }, + }, + }, + } ); +} From 916a8e4909bcfb13a0093f62774e04d1ce391263 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Tue, 16 Aug 2022 18:41:21 +0200 Subject: [PATCH 4/8] Refactor and cleanup the JS demo code Specifically: 1. Creates a `constant.ts` file to store all shared constants. Currently, the default variation attributes. 2. Move the variations to their own directory. One file per variation. 3. Move the inspector controls into own file and create a conditional logic to allow showing only certain settings. --- assets/js/blocks/product-query/constants.ts | 20 +++++++ assets/js/blocks/product-query/index.tsx | 36 ++++++++++++ .../product-query/inspector-controls.tsx | 57 +++++++++++++++++++ .../variations/product-query.tsx | 50 ++++++++++++++++ .../variations/products-on-sale.tsx | 53 +++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 assets/js/blocks/product-query/constants.ts create mode 100644 assets/js/blocks/product-query/index.tsx create mode 100644 assets/js/blocks/product-query/inspector-controls.tsx create mode 100644 assets/js/blocks/product-query/variations/product-query.tsx create mode 100644 assets/js/blocks/product-query/variations/products-on-sale.tsx diff --git a/assets/js/blocks/product-query/constants.ts b/assets/js/blocks/product-query/constants.ts new file mode 100644 index 00000000000..28c3f536115 --- /dev/null +++ b/assets/js/blocks/product-query/constants.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import { QueryBlockQuery } from './types'; + +export const QUERY_DEFAULT_ATTRIBUTES: { query: QueryBlockQuery } = { + query: { + perPage: 6, + pages: 0, + offset: 0, + postType: 'product', + order: 'desc', + orderBy: 'date', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: false, + }, +}; diff --git a/assets/js/blocks/product-query/index.tsx b/assets/js/blocks/product-query/index.tsx new file mode 100644 index 00000000000..dbdb98e2907 --- /dev/null +++ b/assets/js/blocks/product-query/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { Block } from '@wordpress/blocks'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import './inspector-controls'; +import './variations/product-query'; +import './variations/products-on-sale'; + +function registerProductQueryVariationAttributes( + props: Block, + blockName: string +) { + if ( blockName === 'core/query' ) { + // Gracefully handle if settings.attributes is undefined. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore -- We need this because `attributes` is marked as `readonly` + props.attributes = { + ...props.attributes, + __woocommerceVariationProps: { + type: 'object', + }, + }; + } + return props; +} + +addFilter( + 'blocks.registerBlockType', + 'core/custom-class-name/attribute', + registerProductQueryVariationAttributes +); diff --git a/assets/js/blocks/product-query/inspector-controls.tsx b/assets/js/blocks/product-query/inspector-controls.tsx new file mode 100644 index 00000000000..a3e2768b069 --- /dev/null +++ b/assets/js/blocks/product-query/inspector-controls.tsx @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { InspectorControls } from '@wordpress/block-editor'; +import { ToggleControl } from '@wordpress/components'; +import { addFilter } from '@wordpress/hooks'; +import { EditorBlock } from '@woocommerce/types'; +import { ElementType } from 'react'; + +/** + * Internal dependencies + */ +import { ProductQueryBlock } from './types'; +import { isWooQueryBlockVariation, setCustomQueryAttribute } from './utils'; + +export const INSPECTOR_CONTROLS = { + onSale: ( props: ProductQueryBlock ) => ( + { + setCustomQueryAttribute( props, { onSale } ); + } } + /> + ), +}; + +export const withProductQueryControls = + < T extends EditorBlock< T > >( BlockEdit: ElementType ) => + ( props: ProductQueryBlock ) => { + return isWooQueryBlockVariation( props ) ? ( + <> + + + { Object.entries( INSPECTOR_CONTROLS ).map( + ( [ key, Control ] ) => + props.attributes.__woocommerceVariationProps.attributes?.disabledInspectorControls?.includes( + key + ) ? null : ( + + ) + ) } + + + ) : ( + + ); + }; + +addFilter( 'editor.BlockEdit', 'core/query', withProductQueryControls ); diff --git a/assets/js/blocks/product-query/variations/product-query.tsx b/assets/js/blocks/product-query/variations/product-query.tsx new file mode 100644 index 00000000000..74a1f396ac3 --- /dev/null +++ b/assets/js/blocks/product-query/variations/product-query.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { isExperimentalBuild } from '@woocommerce/block-settings'; +import { registerBlockVariation } from '@wordpress/blocks'; +import { Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { sparkles } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { QUERY_DEFAULT_ATTRIBUTES } from '../constants'; + +if ( isExperimentalBuild() ) { + registerBlockVariation( 'core/query', { + name: 'woocommerce/product-query', + title: __( 'Product Query', 'woo-gutenberg-products-block' ), + isActive: ( attributes ) => { + return ( + attributes?.__woocommerceVariationProps?.name === + 'product-query' + ); + }, + icon: { + src: ( + + ), + }, + attributes: { + ...QUERY_DEFAULT_ATTRIBUTES, + __woocommerceVariationProps: { + name: 'product-query', + }, + }, + innerBlocks: [ + [ + 'core/post-template', + {}, + [ [ 'core/post-title' ], [ 'core/post-featured-image' ] ], + ], + [ 'core/query-pagination' ], + [ 'core/query-no-results' ], + ], + scope: [ 'block', 'inserter' ], + } ); +} diff --git a/assets/js/blocks/product-query/variations/products-on-sale.tsx b/assets/js/blocks/product-query/variations/products-on-sale.tsx new file mode 100644 index 00000000000..f5365ca1918 --- /dev/null +++ b/assets/js/blocks/product-query/variations/products-on-sale.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { isExperimentalBuild } from '@woocommerce/block-settings'; +import { registerBlockVariation } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; +import { Icon, percent } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { QUERY_DEFAULT_ATTRIBUTES } from '../constants'; + +if ( isExperimentalBuild() ) { + registerBlockVariation( 'core/query', { + name: 'woocommerce/query-products-on-sale', + title: __( 'Products on Sale', 'woo-gutenberg-products-block' ), + isActive: ( blockAttributes ) => + blockAttributes?.__woocommerceVariationProps?.name === + 'query-products-on-sale' || + blockAttributes?.__woocommerceVariationProps?.query?.onSale === + true, + icon: { + src: ( + + ), + }, + attributes: { + ...QUERY_DEFAULT_ATTRIBUTES, + __woocommerceVariationProps: { + name: 'query-products-on-sale', + attributes: { + query: { + onSale: true, + }, + }, + }, + }, + innerBlocks: [ + [ + 'core/post-template', + {}, + [ [ 'core/post-title' ], [ 'core/post-featured-image' ] ], + ], + [ 'core/query-pagination' ], + [ 'core/query-no-results' ], + ], + scope: [ 'block', 'inserter' ], + } ); +} From b70b80f6b43309b27f4aac6acc4fc6f6be799276 Mon Sep 17 00:00:00 2001 From: Luigi Date: Tue, 16 Aug 2022 18:41:37 +0200 Subject: [PATCH 5/8] Update webpack config --- bin/webpack-configs.js | 2 +- bin/webpack-entries.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 9f16254afa7..4c91754bf8e 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -261,7 +261,7 @@ const getMainConfig = ( options = {} ) => { new CopyWebpackPlugin( { patterns: [ { - from: './assets/js/blocks/**/block.json', + from: './assets/js/**/block.json', to( { absoluteFilename } ) { /** * Getting the block name from the JSON metadata is less error prone diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 714dc064608..4e717d948e0 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -57,6 +57,9 @@ const blocks = { 'legacy-template': { customDir: 'classic-template', }, + 'product-query': { + isExperimental: true, + }, }; // Returns the entries for each block given a relative path (ie: `index.js`, From cf5a57e59d97992bd73c83b8e45fdc5b0f1d67b0 Mon Sep 17 00:00:00 2001 From: Luigi Date: Tue, 16 Aug 2022 18:41:58 +0200 Subject: [PATCH 6/8] Add ProductQuery class --- src/BlockTypes/ProductQuery.php | 111 ++++++++++++++++++++++++++++++++ src/BlockTypesController.php | 1 + 2 files changed, 112 insertions(+) create mode 100644 src/BlockTypes/ProductQuery.php diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php new file mode 100644 index 00000000000..1661640c20c --- /dev/null +++ b/src/BlockTypes/ProductQuery.php @@ -0,0 +1,111 @@ +get_query_by_attributes( $query, $parsed_block ); + }, + 10, + 3 + ); + } + + /** + * Return a custom query based on the attributes. + * + * @param WP_Query $query The WordPress Query. + * @param WP_Block $parsed_block The block being rendered. + * @return array + */ + public function get_query_by_attributes( $query, $parsed_block ) { + if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { + return $query; + } + + $variation_props = $parsed_block['attrs']['__woocommerceVariationProps']; + $common_query_values = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'posts_per_page' => $query['posts_per_page'], + 'orderby' => $query['orderby'], + 'order' => $query['order'], + ); + $on_sale_query = $this->get_on_sale_products_query( $variation_props ); + + return array_merge( $query, $common_query_values, $on_sale_query ); + } + + /** + * Return a query for on sale products. + * + * @param array $variation_props Dedicated attributes for the variation. + * @return array + */ + private function get_on_sale_products_query( $variation_props ) { + if ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) { + return array(); + } + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_sale_price', + 'value' => 0, + 'compare' => '>', + 'type' => 'numeric', + ), + array( + 'key' => '_min_variation_sale_price', + 'value' => 0, + 'compare' => '>', + 'type' => 'numeric', + ), + ), + ); + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 19009e35a10..b8359635e29 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -198,6 +198,7 @@ protected function get_block_types() { 'ProductTitle', 'MiniCart', 'MiniCartContents', + 'ProductQuery', ]; $block_types = array_merge( $block_types, Cart::get_cart_block_types(), Checkout::get_checkout_block_types() ); From 4ffd40ad2c2a12c9dd37226d3e8c11ef2f7b5eaa Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Tue, 16 Aug 2022 19:15:23 +0200 Subject: [PATCH 7/8] Fix `QueryVariation` enum We had changed the Products on Sale variation slug to something else, but we had forgotten to update the proper enum. --- assets/js/blocks/product-query/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/blocks/product-query/types.ts b/assets/js/blocks/product-query/types.ts index 0675128f512..3329005a37d 100644 --- a/assets/js/blocks/product-query/types.ts +++ b/assets/js/blocks/product-query/types.ts @@ -66,7 +66,8 @@ export interface QueryBlockQuery { export enum QueryVariation { /** The main, fully customizable, Product Query block */ PRODUCT_QUERY = 'product-query', - PRODUCTS_ON_SALE = 'query-on-sale', + /** Only shows products on sale */ + PRODUCTS_ON_SALE = 'query-products-on-sale', } export type WooCommerceBlockVariation< T > = EditorBlock< { From d432d6cd257374e11dcf4201beeeef97663318e0 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Wed, 17 Aug 2022 00:01:54 +0200 Subject: [PATCH 8/8] Remove unused params from `update_query` The filter we added to Gutenberg will pass the block and the page, as we might need them in the future and we want to minimize the amount of changes we'll have to do upstream. However, we currently do not use those, so I removed them from our own inner function. --- src/BlockTypes/ProductQuery.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 1661640c20c..bb2b7435152 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -37,14 +37,13 @@ protected function initialize() { * @param array $parsed_block The block being rendered. */ public function update_query( $pre_render, $parsed_block ) { - if ( 'core/query' !== $parsed_block['blockName'] ) { return; } add_filter( 'gutenberg_build_query_vars_from_query_block', - function( $query, $block, $page ) use ( $parsed_block ) { + function( $query ) use ( $parsed_block ) { return $this->get_query_by_attributes( $query, $parsed_block ); }, 10,