From ff85d47109faa91aa8e643bb86271ee59a7a84a7 Mon Sep 17 00:00:00 2001 From: Lucio Giannotta Date: Fri, 16 Sep 2022 20:11:47 +0200 Subject: [PATCH] Refactor Product Query to use the latest Gutenberg APIs As we worked with Gutenberg folks in WordPress/gutenberg#43590, WordPress/gutenberg#/43632 and WordPress/gutenberg#/44093 we have created a standard API that could be used for our use-case. This PR refactors our WIP experimental work to use that standardized API. --- assets/js/blocks/product-query/constants.ts | 28 ++++++----- .../product-query/inspector-controls.tsx | 22 ++++---- assets/js/blocks/product-query/types.ts | 28 ++++++----- assets/js/blocks/product-query/utils.tsx | 50 +++++++++++++++---- .../variations/product-query.tsx | 27 ++++++---- .../variations/products-on-sale.tsx | 37 +++++++++----- 6 files changed, 122 insertions(+), 70 deletions(-) diff --git a/assets/js/blocks/product-query/constants.ts b/assets/js/blocks/product-query/constants.ts index 1c05f1276b9..0ae12ff267d 100644 --- a/assets/js/blocks/product-query/constants.ts +++ b/assets/js/blocks/product-query/constants.ts @@ -1,20 +1,24 @@ /** * External dependencies */ -import { InnerBlockTemplate } from '@wordpress/blocks'; +import type { InnerBlockTemplate } from '@wordpress/blocks'; /** * Internal dependencies */ -import { QueryBlockQuery } from './types'; +import { QueryBlockAttributes } from './types'; -export const QUERY_DEFAULT_ATTRIBUTES: { - query: QueryBlockQuery; - displayLayout: { - type: 'flex' | 'list'; - columns?: number; - }; -} = { +export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'order', 'taxQuery', 'search' ]; + +export const ALL_PRODUCT_QUERY_CONTROLS = [ 'onSale' ]; + +export const DEFAULT_ALLOWED_CONTROLS = [ + ...DEFAULT_CORE_ALLOWED_CONTROLS, + ...ALL_PRODUCT_QUERY_CONTROLS, +]; + +export const QUERY_DEFAULT_ATTRIBUTES: QueryBlockAttributes = { + allowControls: DEFAULT_ALLOWED_CONTROLS, displayLayout: { type: 'flex', columns: 3, @@ -39,7 +43,7 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [ 'core/post-template', {}, [ - [ 'woocommerce/product-image', undefined, [] ], + [ 'woocommerce/product-image' ], [ 'core/post-title', { @@ -50,6 +54,6 @@ export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [ ], ], ], - [ 'core/query-pagination', undefined, [] ], - [ 'core/query-no-results', undefined, [] ], + [ 'core/query-pagination' ], + [ 'core/query-no-results' ], ]; diff --git a/assets/js/blocks/product-query/inspector-controls.tsx b/assets/js/blocks/product-query/inspector-controls.tsx index a3e2768b069..56cfc24100a 100644 --- a/assets/js/blocks/product-query/inspector-controls.tsx +++ b/assets/js/blocks/product-query/inspector-controls.tsx @@ -12,7 +12,11 @@ import { ElementType } from 'react'; * Internal dependencies */ import { ProductQueryBlock } from './types'; -import { isWooQueryBlockVariation, setCustomQueryAttribute } from './utils'; +import { + isWooQueryBlockVariation, + setCustomQueryAttribute, + useAllowedControls, +} from './utils'; export const INSPECTOR_CONTROLS = { onSale: ( props: ProductQueryBlock ) => ( @@ -21,12 +25,9 @@ export const INSPECTOR_CONTROLS = { 'Show only products on sale', 'woo-gutenberg-products-block' ) } - checked={ - props.attributes.__woocommerceVariationProps?.attributes?.query - ?.onSale || false - } - onChange={ ( onSale ) => { - setCustomQueryAttribute( props, { onSale } ); + checked={ props.attributes.query.__woocommerceOnSale || false } + onChange={ ( __woocommerceOnSale ) => { + setCustomQueryAttribute( props, { __woocommerceOnSale } ); } } /> ), @@ -35,17 +36,16 @@ export const INSPECTOR_CONTROLS = { export const withProductQueryControls = < T extends EditorBlock< T > >( BlockEdit: ElementType ) => ( props: ProductQueryBlock ) => { + const allowedControls = useAllowedControls( props.attributes ); return isWooQueryBlockVariation( props ) ? ( <> { Object.entries( INSPECTOR_CONTROLS ).map( ( [ key, Control ] ) => - props.attributes.__woocommerceVariationProps.attributes?.disabledInspectorControls?.includes( - key - ) ? null : ( + allowedControls?.includes( key ) ? ( - ) + ) : null ) } diff --git a/assets/js/blocks/product-query/types.ts b/assets/js/blocks/product-query/types.ts index 3329005a37d..0d1b95fbe26 100644 --- a/assets/js/blocks/product-query/types.ts +++ b/assets/js/blocks/product-query/types.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import { BlockInstance } from '@wordpress/blocks'; import type { EditorBlock } from '@woocommerce/types'; export interface ProductQueryArguments { @@ -28,11 +27,14 @@ export interface ProductQueryArguments { * ) * ``` */ - onSale?: boolean; + // 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 + __woocommerceOnSale?: boolean; } -export type ProductQueryBlock = - WooCommerceBlockVariation< ProductQueryAttributes >; +export type ProductQueryBlock = EditorBlock< QueryBlockAttributes >; export interface ProductQueryAttributes { /** @@ -47,6 +49,16 @@ export interface ProductQueryAttributes { query?: ProductQueryArguments; } +export interface QueryBlockAttributes { + allowControls?: string[]; + displayLayout?: { + type: 'flex' | 'list'; + columns?: number; + }; + namespace?: string; + query: QueryBlockQuery & ProductQueryArguments; +} + export interface QueryBlockQuery { author?: string; exclude?: string[]; @@ -69,11 +81,3 @@ export enum QueryVariation { /** Only shows products on sale */ PRODUCTS_ON_SALE = 'query-products-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 > >; -} >; diff --git a/assets/js/blocks/product-query/utils.tsx b/assets/js/blocks/product-query/utils.tsx index 61be85d5e38..cab7ddd49c7 100644 --- a/assets/js/blocks/product-query/utils.tsx +++ b/assets/js/blocks/product-query/utils.tsx @@ -1,3 +1,9 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as WP_BLOCKS_STORE } from '@wordpress/blocks'; + /** * Internal dependencies */ @@ -7,6 +13,13 @@ import { QueryVariation, } from './types'; +/** + * Creates an array that is the symmetric difference of the given arrays + */ +export function ArrayXOR< T extends Array< unknown > >( a: T, b: T ) { + return a.filter( ( el ) => ! b.includes( el ) ); +} + /** * Identifies if a block is a Query block variation from our conventions * @@ -35,20 +48,35 @@ export function isWooQueryBlockVariation( block: ProductQueryBlock ) { */ export function setCustomQueryAttribute( block: ProductQueryBlock, - attributes: Partial< ProductQueryArguments > + queryParams: Partial< ProductQueryArguments > ) { - const { __woocommerceVariationProps } = block.attributes; + const { query } = block.attributes; block.setAttributes( { - __woocommerceVariationProps: { - ...__woocommerceVariationProps, - attributes: { - ...__woocommerceVariationProps.attributes, - query: { - ...__woocommerceVariationProps.attributes?.query, - ...attributes, - }, - }, + query: { + ...query, + ...queryParams, }, } ); } + +/** + * Hook that returns the query properties' names defined by the active + * block variation, to determine which block inspector controls to show. + * + * @param {Object} attributes Block attributes. + * @return {string[]} An array of the controls keys. + */ +export function useAllowedControls( + attributes: ProductQueryBlock[ 'attributes' ] +) { + return useSelect( + ( select ) => + select( WP_BLOCKS_STORE ).getActiveBlockVariation( + 'core/query', + attributes + )?.allowControls, + + [ attributes ] + ); +} diff --git a/assets/js/blocks/product-query/variations/product-query.tsx b/assets/js/blocks/product-query/variations/product-query.tsx index 3a53dbd9afc..83c46240ef6 100644 --- a/assets/js/blocks/product-query/variations/product-query.tsx +++ b/assets/js/blocks/product-query/variations/product-query.tsx @@ -10,18 +10,20 @@ import { sparkles } from '@wordpress/icons'; /** * Internal dependencies */ -import { INNER_BLOCKS_TEMPLATE, QUERY_DEFAULT_ATTRIBUTES } from '../constants'; +import { + DEFAULT_ALLOWED_CONTROLS, + INNER_BLOCKS_TEMPLATE, + QUERY_DEFAULT_ATTRIBUTES, +} from '../constants'; + +const VARIATION_NAME = 'woocommerce/product-query'; if ( isExperimentalBuild() ) { registerBlockVariation( 'core/query', { - name: 'woocommerce/product-query', + name: VARIATION_NAME, title: __( 'Product Query', 'woo-gutenberg-products-block' ), - isActive: ( attributes ) => { - return ( - attributes?.__woocommerceVariationProps?.name === - 'product-query' - ); - }, + isActive: ( blockAttributes ) => + blockAttributes.namespace === VARIATION_NAME, icon: { src: ( - blockAttributes?.__woocommerceVariationProps?.name === - 'query-products-on-sale' || - blockAttributes?.__woocommerceVariationProps?.query?.onSale === - true, + blockAttributes.namespace === VARIATION_NAME || + blockAttributes.query?.__woocommerceOnSale === true, icon: { src: (