Implement Hand-Picked Products block (https://github.com/woocommerce/woocommerce-blocks/pull/7925)
Implements the ProductSelector advanced filter within the “Products (Beta)” block. The filter allows the merchant to narrow down the exact products to which all subsequent filters will be applied, mirroring the functionality of the existing “Hand-picked Products” plus all the other functionalities available from the “Products (Beta)” block.
This commit is contained in:
parent
4c204ab578
commit
41817ea2d6
|
@ -2,6 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
import { objectOmit } from '@woocommerce/utils';
|
||||||
import type { InnerBlockTemplate } from '@wordpress/blocks';
|
import type { InnerBlockTemplate } from '@wordpress/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,15 +13,6 @@ import { VARIATION_NAME as PRODUCT_TITLE_ID } from './variations/elements/produc
|
||||||
import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template';
|
import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template';
|
||||||
import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';
|
import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object without a key.
|
|
||||||
*/
|
|
||||||
function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
|
|
||||||
const { [ key ]: omit, ...rest } = obj;
|
|
||||||
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EDIT_ATTRIBUTES_URL =
|
export const EDIT_ATTRIBUTES_URL =
|
||||||
'/wp-admin/edit.php?post_type=product&page=product_attributes';
|
'/wp-admin/edit.php?post_type=product&page=product_attributes';
|
||||||
|
|
||||||
|
@ -31,6 +23,7 @@ export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'taxQuery', 'search' ];
|
||||||
export const ALL_PRODUCT_QUERY_CONTROLS = [
|
export const ALL_PRODUCT_QUERY_CONTROLS = [
|
||||||
'attributes',
|
'attributes',
|
||||||
'presets',
|
'presets',
|
||||||
|
'productSelector',
|
||||||
'onSale',
|
'onSale',
|
||||||
'stockStatus',
|
'stockStatus',
|
||||||
'wooInherit',
|
'wooInherit',
|
||||||
|
|
|
@ -38,8 +38,9 @@ import {
|
||||||
QUERY_LOOP_ID,
|
QUERY_LOOP_ID,
|
||||||
STOCK_STATUS_OPTIONS,
|
STOCK_STATUS_OPTIONS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { PopularPresets } from './inspector-controls/popular-presets';
|
|
||||||
import { AttributesFilter } from './inspector-controls/attributes-filter';
|
import { AttributesFilter } from './inspector-controls/attributes-filter';
|
||||||
|
import { PopularPresets } from './inspector-controls/popular-presets';
|
||||||
|
import { ProductSelector } from './inspector-controls/product-selector';
|
||||||
|
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
|
||||||
|
@ -168,6 +169,7 @@ export const TOOLS_PANEL_CONTROLS = {
|
||||||
</ToolsPanelItem>
|
</ToolsPanelItem>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
productSelector: ProductSelector,
|
||||||
stockStatus: ( props: ProductQueryBlock ) => {
|
stockStatus: ( props: ProductQueryBlock ) => {
|
||||||
const { query } = props.attributes;
|
const { query } = props.attributes;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getProducts } from '@woocommerce/editor-components/utils';
|
||||||
|
import { ProductResponseItem } from '@woocommerce/types';
|
||||||
|
import { objectOmit } from '@woocommerce/utils';
|
||||||
|
import { useState, useEffect } from '@wordpress/element';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
FormTokenField,
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalToolsPanelItem as ToolsPanelItem,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ProductQueryBlock } from '../types';
|
||||||
|
import { setQueryAttribute } from '../utils';
|
||||||
|
|
||||||
|
function useProductsList() {
|
||||||
|
const [ productsList, setProductsList ] = useState< ProductResponseItem[] >(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
getProducts( { selected: [] } ).then( ( results ) => {
|
||||||
|
setProductsList( results as ProductResponseItem[] );
|
||||||
|
} );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
return productsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductSelector = ( props: ProductQueryBlock ) => {
|
||||||
|
const { query } = props.attributes;
|
||||||
|
|
||||||
|
const productsList = useProductsList();
|
||||||
|
|
||||||
|
const onTokenChange = ( values: FormTokenField.Value[] ) => {
|
||||||
|
const ids = values
|
||||||
|
.map(
|
||||||
|
( nameOrId ) =>
|
||||||
|
productsList.find(
|
||||||
|
( product ) =>
|
||||||
|
product.name === nameOrId ||
|
||||||
|
product.id === Number( nameOrId )
|
||||||
|
)?.id
|
||||||
|
)
|
||||||
|
.filter( Boolean )
|
||||||
|
.map( String );
|
||||||
|
|
||||||
|
if ( ! ids.length && props.attributes.query.include ) {
|
||||||
|
const prunedQuery = objectOmit( props.attributes.query, 'include' );
|
||||||
|
|
||||||
|
setQueryAttribute(
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
attributes: {
|
||||||
|
...props.attributes,
|
||||||
|
query: prunedQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setQueryAttribute( props, {
|
||||||
|
include: ids,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolsPanelItem
|
||||||
|
label={ __(
|
||||||
|
'Hand-picked Products',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
) }
|
||||||
|
hasValue={ () => query.include?.length }
|
||||||
|
>
|
||||||
|
<FormTokenField
|
||||||
|
disabled={ ! productsList.length }
|
||||||
|
displayTransform={ ( token: string ) =>
|
||||||
|
Number.isNaN( Number( token ) )
|
||||||
|
? token
|
||||||
|
: productsList.find(
|
||||||
|
( product ) => product.id === Number( token )
|
||||||
|
)?.name || ''
|
||||||
|
}
|
||||||
|
label={ __(
|
||||||
|
'Pick some products',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
) }
|
||||||
|
onChange={ onTokenChange }
|
||||||
|
suggestions={ productsList.map( ( product ) => product.name ) }
|
||||||
|
validateInput={ ( value: string ) =>
|
||||||
|
productsList.find( ( product ) => product.name === value )
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
! productsList.length
|
||||||
|
? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ]
|
||||||
|
: query?.include || []
|
||||||
|
}
|
||||||
|
__experimentalExpandOnFocus={ true }
|
||||||
|
/>
|
||||||
|
</ToolsPanelItem>
|
||||||
|
);
|
||||||
|
};
|
|
@ -80,6 +80,7 @@ export interface QueryBlockAttributes {
|
||||||
export interface QueryBlockQuery {
|
export interface QueryBlockQuery {
|
||||||
author?: string;
|
author?: string;
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
|
include?: string[];
|
||||||
inherit: boolean;
|
inherit: boolean;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
order: 'asc' | 'desc';
|
order: 'asc' | 'desc';
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from './attributes-query';
|
||||||
export * from './attributes';
|
export * from './attributes';
|
||||||
export * from './filters';
|
export * from './filters';
|
||||||
export * from './notices';
|
export * from './notices';
|
||||||
|
export * from './object-operations';
|
||||||
export * from './products';
|
export * from './products';
|
||||||
export * from './shared-attributes';
|
export * from './shared-attributes';
|
||||||
export * from './sanitize-html';
|
export * from './sanitize-html';
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Returns an object without a key.
|
||||||
|
*/
|
||||||
|
export function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
|
||||||
|
const { [ key ]: omit, ...rest } = obj;
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
}
|
|
@ -165,18 +165,21 @@ class ProductQuery extends AbstractBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
$common_query_values = array(
|
$common_query_values = array(
|
||||||
'post_type' => 'product',
|
'meta_query' => array(),
|
||||||
'post__in' => array(),
|
|
||||||
'post_status' => 'publish',
|
|
||||||
'posts_per_page' => $query['posts_per_page'],
|
'posts_per_page' => $query['posts_per_page'],
|
||||||
'orderby' => $query['orderby'],
|
'orderby' => $query['orderby'],
|
||||||
'order' => $query['order'],
|
'order' => $query['order'],
|
||||||
'offset' => $query['offset'],
|
'offset' => $query['offset'],
|
||||||
'meta_query' => array(),
|
'post__in' => array(),
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_type' => 'product',
|
||||||
'tax_query' => array(),
|
'tax_query' => array(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->merge_queries(
|
$handpicked_products = isset( $parsed_block['attrs']['query']['include'] ) ?
|
||||||
|
$parsed_block['attrs']['query']['include'] : $common_query_values['post__in'];
|
||||||
|
|
||||||
|
$merged_query = $this->merge_queries(
|
||||||
$common_query_values,
|
$common_query_values,
|
||||||
$this->get_global_query( $parsed_block ),
|
$this->get_global_query( $parsed_block ),
|
||||||
$this->get_custom_orderby_query( $query['orderby'] ),
|
$this->get_custom_orderby_query( $query['orderby'] ),
|
||||||
|
@ -185,6 +188,8 @@ class ProductQuery extends AbstractBlock {
|
||||||
$this->get_filter_by_taxonomies_query( $query ),
|
$this->get_filter_by_taxonomies_query( $query ),
|
||||||
$this->get_filter_by_keyword_query( $query )
|
$this->get_filter_by_keyword_query( $query )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -307,6 +312,23 @@ class ProductQuery extends AbstractBlock {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the query only to a subset of products
|
||||||
|
*
|
||||||
|
* @param array $query The query.
|
||||||
|
* @param array $ids Array of selected product ids.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function filter_query_to_only_include_ids( $query, $ids ) {
|
||||||
|
if ( ! empty( $ids ) ) {
|
||||||
|
$query['post__in'] = empty( $query['post__in'] ) ?
|
||||||
|
$ids : array_intersect( $ids, $query['post__in'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the `tax_query` for the requested attributes
|
* Return the `tax_query` for the requested attributes
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue