diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
index 2876e73f837..057ce3234f9 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
@@ -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: [],
};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/hand-picked-products-control.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/hand-picked-products-control.tsx
new file mode 100644
index 00000000000..b14ca82b2a0
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/hand-picked-products-control.tsx
@@ -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 (
+ !! selectedProductIds?.length }
+ onDeselect={ () => {
+ setQueryAttribute( {
+ woocommerceHandPickedProducts: [],
+ } );
+ } }
+ >
+
+ productsMap.has( value )
+ }
+ value={
+ ! productsMap.size
+ ? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ]
+ : selectedProductIds || []
+ }
+ />
+
+ );
+};
+
+export default HandPickedProductsControl;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/index.tsx
index a0743cbcb99..7f4425cc9af 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/inspector-controls/index.tsx
@@ -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 = (
>
+
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;
+ }
}