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