Product Collection: Hand picked products control in sidebar settings (https://github.com/woocommerce/woocommerce-blocks/pull/9733)

* Add support for hand-picked products in Product Collection block

This commit introduces the ability to manually select specific products in the Product Collection block.

Changes include:
- Added `woocommerceHandPickedProducts` to the `ProductCollectionQuery` and `DEFAULT_FILTERS` in `constants.ts`.
- Created a new control file `hand-picked-products-control.tsx` which introduces a token field where the user can search for and select specific products.
- Included `HandPickedProductsControl` in the Product Collection block's inspector controls in `index.tsx`.
- Updated `ProductCollectionQuery` in `types.ts` to accommodate handpicked products.
- Updated the PHP `ProductCollection` class to handle the hand-picked products query parameters.

These changes allow users to hand-pick products to be displayed in the Product Collection block. This allows for greater customization of the products shown in the block.

* Enhance handling of hand-picked products

- A Set data structure is now used to store 'newHandPickedProducts' instead of an Array, which will help prevent duplicate entries.
- Additionally, the suggestions for products to be hand-picked now excludes already selected products, enhancing the user experience by avoiding redundancy in the suggestions list.
- Lastly, the function name 'displayTransform' has been changed to 'transformTokenIntoProductName' to more accurately reflect its purpose, which is to transform a token into a product name.

* Update import & export of HandPickedProductsControl
This commit is contained in:
Manish Menaria 2023-06-08 11:33:01 +05:30 committed by GitHub
parent 1814bc95e5
commit d91385e3b9
5 changed files with 205 additions and 25 deletions

View File

@ -49,6 +49,7 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
woocommerceOnSale: false,
woocommerceStockStatus: getDefaultStockStatuses(),
woocommerceAttributes: [],
woocommerceHandPickedProducts: [],
};
export const DEFAULT_ATTRIBUTES: Partial< ProductCollectionAttributes > = {
@ -73,9 +74,10 @@ export const getDefaultSettings = (
},
} );
export const DEFAULT_FILTERS = {
export const DEFAULT_FILTERS: Partial< ProductCollectionQuery > = {
woocommerceOnSale: DEFAULT_QUERY.woocommerceOnSale,
woocommerceStockStatus: getDefaultStockStatuses(),
woocommerceAttributes: [],
taxQuery: DEFAULT_QUERY.taxQuery,
woocommerceHandPickedProducts: [],
};

View File

@ -0,0 +1,146 @@
/**
* External dependencies
*/
import { getProducts } from '@woocommerce/editor-components/utils';
import { ProductResponseItem } from '@woocommerce/types';
import { useState, useEffect, useCallback, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
FormTokenField,
// @ts-expect-error Using experimental features
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { ProductCollectionQuery } from '../types';
interface HandPickedProductsControlProps {
setQueryAttribute: ( value: Partial< ProductCollectionQuery > ) => void;
selectedProductIds?: string[] | undefined;
}
/**
* Returns:
* - productsMap: Map of products by id and name.
* - productsList: List of products retrieved.
*/
function useProducts() {
// Creating a map for fast lookup of products by id or name.
const [ productsMap, setProductsMap ] = useState<
Map< number | string, ProductResponseItem >
>( new Map() );
// List of products retrieved
const [ productsList, setProductsList ] = useState< ProductResponseItem[] >(
[]
);
useEffect( () => {
getProducts( { selected: [] } ).then( ( results ) => {
const newProductsMap = new Map();
( results as ProductResponseItem[] ).forEach( ( product ) => {
newProductsMap.set( product.id, product );
newProductsMap.set( product.name, product );
} );
setProductsList( results as ProductResponseItem[] );
setProductsMap( newProductsMap );
} );
}, [] );
return { productsMap, productsList };
}
const HandPickedProductsControl = ( {
selectedProductIds,
setQueryAttribute,
}: HandPickedProductsControlProps ) => {
const { productsMap, productsList } = useProducts();
const onTokenChange = useCallback(
( values: string[] ) => {
// Map the tokens to product ids.
const newHandPickedProductsSet = values.reduce(
( acc, nameOrId ) => {
const product =
productsMap.get( nameOrId ) ||
productsMap.get( Number( nameOrId ) );
if ( product ) acc.add( String( product.id ) );
return acc;
},
new Set< string >()
);
setQueryAttribute( {
woocommerceHandPickedProducts: Array.from(
newHandPickedProductsSet
),
} );
},
[ setQueryAttribute, productsMap ]
);
const suggestions = useMemo( () => {
return (
productsList
// Filter out products that are already selected.
.filter(
( product ) =>
! selectedProductIds?.includes( String( product.id ) )
)
.map( ( product ) => product.name )
);
}, [ productsList, selectedProductIds ] );
const transformTokenIntoProductName = ( token: string ) => {
const parsedToken = Number( token );
if ( Number.isNaN( parsedToken ) ) {
return token;
}
const product = productsMap.get( parsedToken );
return product?.name || '';
};
return (
<ToolsPanelItem
label={ __(
'Hand-picked Products',
'woo-gutenberg-products-block'
) }
hasValue={ () => !! selectedProductIds?.length }
onDeselect={ () => {
setQueryAttribute( {
woocommerceHandPickedProducts: [],
} );
} }
>
<FormTokenField
disabled={ ! productsMap.size }
displayTransform={ transformTokenIntoProductName }
label={ __(
'Pick some products',
'woo-gutenberg-products-block'
) }
onChange={ onTokenChange }
suggestions={ suggestions }
// @ts-expect-error Using experimental features
__experimentalValidateInput={ ( value: string ) =>
productsMap.has( value )
}
value={
! productsMap.size
? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ]
: selectedProductIds || []
}
/>
</ToolsPanelItem>
);
};
export default HandPickedProductsControl;

View File

@ -25,6 +25,7 @@ import StockStatusControl from './stock-status-control';
import KeywordControl from './keyword-control';
import AttributesControl from './attributes-control';
import TaxonomyControls from './taxonomy-controls';
import HandPickedProductsControl from './hand-picked-products-control';
import AuthorControl from './author-control';
const ProductCollectionInspectorControls = (
@ -73,6 +74,12 @@ const ProductCollectionInspectorControls = (
>
<OnSaleControl { ...props } />
<StockStatusControl { ...props } />
<HandPickedProductsControl
setQueryAttribute={ setQueryAttributeBind }
selectedProductIds={
query.woocommerceHandPickedProducts
}
/>
<KeywordControl { ...props } />
<AttributesControl
woocommerceAttributes={

View File

@ -52,6 +52,7 @@ export interface ProductCollectionQuery {
woocommerceStockStatus?: string[];
woocommerceAttributes?: AttributeMetadata[];
isProductCollectionBlock?: boolean;
woocommerceHandPickedProducts?: string[];
}
export type TProductCollectionOrder = 'asc' | 'desc';

View File

@ -76,19 +76,21 @@ class ProductCollection extends AbstractBlock {
return $args;
}
$orderby = $request->get_param( 'orderBy' );
$on_sale = $request->get_param( 'woocommerceOnSale' ) === 'true';
$stock_status = $request->get_param( 'woocommerceStockStatus' );
$product_attributes = $request->get_param( 'woocommerceAttributes' );
$args['author'] = $request->get_param( 'author' ) ?? '';
$orderby = $request->get_param( 'orderBy' );
$on_sale = $request->get_param( 'woocommerceOnSale' ) === 'true';
$stock_status = $request->get_param( 'woocommerceStockStatus' );
$product_attributes = $request->get_param( 'woocommerceAttributes' );
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
$args['author'] = $request->get_param( 'author' ) ?? '';
return $this->get_final_query_args(
$args,
array(
'orderby' => $orderby,
'on_sale' => $on_sale,
'stock_status' => $stock_status,
'product_attributes' => $product_attributes,
'orderby' => $orderby,
'on_sale' => $on_sale,
'stock_status' => $stock_status,
'product_attributes' => $product_attributes,
'handpicked_products' => $handpicked_products,
)
);
}
@ -100,15 +102,18 @@ class ProductCollection extends AbstractBlock {
* @param array $query Query from block context.
*/
private function get_final_query_args( $common_query_values, $query ) {
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : [];
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query ) : [];
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
$taxonomies_query = $query['taxonomies_query'] ?? [];
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query );
$handpicked_products = $query['handpicked_products'] ?? [];
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : [];
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query ) : [];
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
$taxonomies_query = $query['taxonomies_query'] ?? [];
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query );
return $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query );
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query );
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
}
/**
@ -147,17 +152,19 @@ class ProductCollection extends AbstractBlock {
'author' => $block_context_query['author'] ?? '',
);
$is_on_sale = $block_context_query['woocommerceOnSale'] ?? false;
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
$is_on_sale = $block_context_query['woocommerceOnSale'] ?? false;
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
$handpicked_products = $block_context_query['woocommerceHandPickedProducts'] ?? [];
return $this->get_final_query_args(
$common_query_values,
array(
'on_sale' => $is_on_sale,
'stock_status' => $block_context_query['woocommerceStockStatus'],
'orderby' => $block_context_query['orderBy'],
'product_attributes' => $block_context_query['woocommerceAttributes'],
'taxonomies_query' => $taxonomies_query,
'on_sale' => $is_on_sale,
'stock_status' => $block_context_query['woocommerceStockStatus'],
'orderby' => $block_context_query['orderBy'],
'product_attributes' => $block_context_query['woocommerceAttributes'],
'taxonomies_query' => $taxonomies_query,
'handpicked_products' => $handpicked_products,
)
);
}
@ -541,4 +548,21 @@ class ProductCollection extends AbstractBlock {
// phpcs:ignore WordPress.DB.SlowDBQuery
return ! empty( $result ) ? [ 'tax_query' => $result ] : [];
}
/**
* 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;
}
}