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:
Lucio Giannotta 2023-05-08 14:50:10 +02:00 committed by GitHub
parent 4c204ab578
commit 41817ea2d6
7 changed files with 150 additions and 15 deletions

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import { objectOmit } from '@woocommerce/utils';
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 { 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 =
'/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 = [
'attributes',
'presets',
'productSelector',
'onSale',
'stockStatus',
'wooInherit',

View File

@ -38,8 +38,9 @@ import {
QUERY_LOOP_ID,
STOCK_STATUS_OPTIONS,
} from './constants';
import { PopularPresets } from './inspector-controls/popular-presets';
import { AttributesFilter } from './inspector-controls/attributes-filter';
import { PopularPresets } from './inspector-controls/popular-presets';
import { ProductSelector } from './inspector-controls/product-selector';
import './editor.scss';
@ -168,6 +169,7 @@ export const TOOLS_PANEL_CONTROLS = {
</ToolsPanelItem>
);
},
productSelector: ProductSelector,
stockStatus: ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

View File

@ -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>
);
};

View File

@ -80,6 +80,7 @@ export interface QueryBlockAttributes {
export interface QueryBlockQuery {
author?: string;
exclude?: string[];
include?: string[];
inherit: boolean;
offset?: number;
order: 'asc' | 'desc';

View File

@ -3,6 +3,7 @@ export * from './attributes-query';
export * from './attributes';
export * from './filters';
export * from './notices';
export * from './object-operations';
export * from './products';
export * from './shared-attributes';
export * from './sanitize-html';

View File

@ -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;
}

View File

@ -165,18 +165,21 @@ class ProductQuery extends AbstractBlock {
}
$common_query_values = array(
'post_type' => 'product',
'post__in' => array(),
'post_status' => 'publish',
'meta_query' => array(),
'posts_per_page' => $query['posts_per_page'],
'orderby' => $query['orderby'],
'order' => $query['order'],
'offset' => $query['offset'],
'meta_query' => array(),
'post__in' => array(),
'post_status' => 'publish',
'post_type' => 'product',
'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,
$this->get_global_query( $parsed_block ),
$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_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
*