Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
Product Query Block POC (Phase 1) (#6812)
Browse files Browse the repository at this point in the history
* 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.

* 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.

* 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.

* 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.

* Update webpack config

* Add ProductQuery class

* Fix `QueryVariation` enum

We had changed the Products on Sale variation slug to something else,
but we had forgotten to update the proper enum.

* 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.

Co-authored-by: Lucio Giannotta <[email protected]>
  • Loading branch information
gigitux and sunyatasattva authored Aug 18, 2022
1 parent 986b3cf commit 7757b85
Show file tree
Hide file tree
Showing 12 changed files with 467 additions and 1 deletion.
20 changes: 20 additions & 0 deletions assets/js/blocks/product-query/constants.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
36 changes: 36 additions & 0 deletions assets/js/blocks/product-query/index.tsx
Original file line number Diff line number Diff line change
@@ -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
);
57 changes: 57 additions & 0 deletions assets/js/blocks/product-query/inspector-controls.tsx
Original file line number Diff line number Diff line change
@@ -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 ) => (
<ToggleControl
label={ __(
'Show only products on sale',
'woo-gutenberg-products-block'
) }
checked={
props.attributes.__woocommerceVariationProps?.attributes?.query
?.onSale || false
}
onChange={ ( onSale ) => {
setCustomQueryAttribute( props, { onSale } );
} }
/>
),
};

export const withProductQueryControls =
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
( props: ProductQueryBlock ) => {
return isWooQueryBlockVariation( props ) ? (
<>
<BlockEdit { ...props } />
<InspectorControls>
{ Object.entries( INSPECTOR_CONTROLS ).map(
( [ key, Control ] ) =>
props.attributes.__woocommerceVariationProps.attributes?.disabledInspectorControls?.includes(
key
) ? null : (
<Control { ...props } />
)
) }
</InspectorControls>
</>
) : (
<BlockEdit { ...props } />
);
};

addFilter( 'editor.BlockEdit', 'core/query', withProductQueryControls );
79 changes: 79 additions & 0 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* 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',
/** 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 > >;
} >;
54 changes: 54 additions & 0 deletions assets/js/blocks/product-query/utils.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
} );
}
50 changes: 50 additions & 0 deletions assets/js/blocks/product-query/variations/product-query.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<Icon
icon={ sparkles }
className="wc-block-editor-components-block-icon wc-block-editor-components-block-icon--sparkles"
/>
),
},
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' ],
} );
}
53 changes: 53 additions & 0 deletions assets/js/blocks/product-query/variations/products-on-sale.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<Icon
icon={ percent }
className="wc-block-editor-components-block-icon wc-block-editor-components-block-icon--percent"
/>
),
},
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' ],
} );
}
3 changes: 3 additions & 0 deletions assets/js/types/type-defs/blocks.ts
Original file line number Diff line number Diff line change
@@ -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 )
Expand Down
2 changes: 1 addition & 1 deletion bin/webpack-configs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bin/webpack-entries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Loading

0 comments on commit 7757b85

Please sign in to comment.