Refactor Product Query to use the latest Gutenberg APIs (https://github.com/woocommerce/woocommerce-blocks/pull/7169)

* Refactor Product Query to use the latest Gutenberg APIs

As we worked with Gutenberg folks in WordPress/gutenbergwoocommerce/woocommerce-blocks#43590,
WordPress/gutenbergwoocommerce/woocommerce-blocks#43632 and WordPress/gutenbergwoocommerce/woocommerce-blocks#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.
This commit is contained in:
Lucio Giannotta 2022-09-23 15:07:44 +02:00 committed by GitHub
parent 6b0407d200
commit d174051787
7 changed files with 142 additions and 82 deletions

View File

@ -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' ],
];

View File

@ -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 ) ? (
<>
<BlockEdit { ...props } />
<InspectorControls>
{ Object.entries( INSPECTOR_CONTROLS ).map(
( [ key, Control ] ) =>
props.attributes.__woocommerceVariationProps.attributes?.disabledInspectorControls?.includes(
key
) ? null : (
allowedControls?.includes( key ) ? (
<Control { ...props } />
)
) : null
) }
</InspectorControls>
</>

View File

@ -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[];
@ -65,15 +77,7 @@ export interface QueryBlockQuery {
export enum QueryVariation {
/** The main, fully customizable, Product Query block */
PRODUCT_QUERY = 'product-query',
PRODUCT_QUERY = 'woocommerce/product-query',
/** Only shows products on sale */
PRODUCTS_ON_SALE = 'query-products-on-sale',
PRODUCTS_ON_SALE = 'woocommerce/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 > >;
} >;

View File

@ -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
*
@ -17,10 +30,8 @@ import {
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
block.attributes.namespace as QueryVariation
)
);
}
@ -35,20 +46,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,
...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 ]
);
}

View File

@ -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: (
<Icon
@ -32,10 +34,13 @@ if ( isExperimentalBuild() ) {
},
attributes: {
...QUERY_DEFAULT_ATTRIBUTES,
__woocommerceVariationProps: {
name: 'product-query',
},
namespace: VARIATION_NAME,
},
// 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
allowControls: DEFAULT_ALLOWED_CONTROLS,
innerBlocks: INNER_BLOCKS_TEMPLATE,
scope: [ 'block', 'inserter' ],
} );

View File

@ -9,17 +9,23 @@ import { Icon, percent } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { INNER_BLOCKS_TEMPLATE, QUERY_DEFAULT_ATTRIBUTES } from '../constants';
import {
DEFAULT_CORE_ALLOWED_CONTROLS,
INNER_BLOCKS_TEMPLATE,
QUERY_DEFAULT_ATTRIBUTES,
} from '../constants';
import { ArrayXOR } from '../utils';
const VARIATION_NAME = 'woocommerce/query-products-on-sale';
const DISABLED_INSPECTOR_CONTROLS = [ 'onSale' ];
if ( isExperimentalBuild() ) {
registerBlockVariation( 'core/query', {
name: 'woocommerce/query-products-on-sale',
name: VARIATION_NAME,
title: __( 'Products on Sale', 'woo-gutenberg-products-block' ),
isActive: ( blockAttributes ) =>
blockAttributes?.__woocommerceVariationProps?.name ===
'query-products-on-sale' ||
blockAttributes?.__woocommerceVariationProps?.query?.onSale ===
true,
blockAttributes.namespace === VARIATION_NAME ||
blockAttributes.query?.__woocommerceOnSale === true,
icon: {
src: (
<Icon
@ -30,15 +36,20 @@ if ( isExperimentalBuild() ) {
},
attributes: {
...QUERY_DEFAULT_ATTRIBUTES,
__woocommerceVariationProps: {
name: 'query-products-on-sale',
attributes: {
namespace: VARIATION_NAME,
query: {
onSale: true,
},
},
...QUERY_DEFAULT_ATTRIBUTES.query,
__woocommerceOnSale: true,
},
},
// 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
allowControls: ArrayXOR(
DEFAULT_CORE_ALLOWED_CONTROLS,
DISABLED_INSPECTOR_CONTROLS
),
innerBlocks: INNER_BLOCKS_TEMPLATE,
scope: [ 'block', 'inserter' ],
} );

View File

@ -37,6 +37,17 @@ class ProductQuery extends AbstractBlock {
}
/**
* Check if a given block
*
* @param array $parsed_block The block being rendered.
* @return boolean
*/
private function is_woocommerce_variation( $parsed_block ) {
return isset( $parsed_block['attrs']['namespace'] )
&& substr( $parsed_block['attrs']['namespace'], 0, 11 ) === 'woocommerce';
}
/**
* Update the query for the product query block.
@ -51,7 +62,7 @@ class ProductQuery extends AbstractBlock {
$this->parsed_block = $parsed_block;
if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
if ( $this->is_woocommerce_variation( $parsed_block ) ) {
add_filter(
'query_loop_block_query_vars',
array( $this, 'get_query_by_attributes' ),
@ -69,11 +80,10 @@ class ProductQuery extends AbstractBlock {
*/
public function get_query_by_attributes( $query ) {
$parsed_block = $this->parsed_block;
if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
if ( ! $this->is_woocommerce_variation( $parsed_block ) ) {
return $query;
}
$variation_props = $parsed_block['attrs']['__woocommerceVariationProps'];
$common_query_values = array(
'post_type' => 'product',
'post_status' => 'publish',
@ -81,7 +91,7 @@ class ProductQuery extends AbstractBlock {
'orderby' => $query['orderby'],
'order' => $query['order'],
);
$on_sale_query = $this->get_on_sale_products_query( $variation_props );
$on_sale_query = $this->get_on_sale_products_query( $parsed_block['attrs']['query'] );
return array_merge( $query, $common_query_values, $on_sale_query );
}
@ -89,11 +99,11 @@ class ProductQuery extends AbstractBlock {
/**
* Return a query for on sale products.
*
* @param array $variation_props Dedicated attributes for the variation.
* @param array $query_params Block query parameters.
* @return array
*/
private function get_on_sale_products_query( $variation_props ) {
if ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) {
private function get_on_sale_products_query( $query_params ) {
if ( ! isset( $query_params['__woocommerceOnSale'] ) || true !== $query_params['__woocommerceOnSale'] ) {
return array();
}