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

Commit

Permalink
POC Product Query block
Browse files Browse the repository at this point in the history
  • Loading branch information
gigitux committed Aug 16, 2022
1 parent 7b165d0 commit 543bf24
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 );
78 changes: 78 additions & 0 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
@@ -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 > >;
} >;
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 543bf24

Please sign in to comment.