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:
parent
1814bc95e5
commit
d91385e3b9
|
@ -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: [],
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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={
|
||||
|
|
|
@ -52,6 +52,7 @@ export interface ProductCollectionQuery {
|
|||
woocommerceStockStatus?: string[];
|
||||
woocommerceAttributes?: AttributeMetadata[];
|
||||
isProductCollectionBlock?: boolean;
|
||||
woocommerceHandPickedProducts?: string[];
|
||||
}
|
||||
|
||||
export type TProductCollectionOrder = 'asc' | 'desc';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue