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

Product Query Block POC (Phase 1) #6812

Merged
merged 8 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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