Commit 22e4ee90c2 for woocommerce

commit 22e4ee90c24640c2865c58117887c1137c8dd360
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Wed Jan 28 15:08:19 2026 +0100

    Product Collection: Handpicked Products should start with product picker (#62989)

    * Foundation for product picker

    * Updated handpicked product picker

    * Move the collection specific controls to the top of filters

    * Add changelog

    * Update docs etc

    * Bring back the comment

    * Prefix editor side CSS classes with .wc-block-editor-

    * Rename product picker files to be less confusing

    * Make Done button always visible but only disabled when no products is chosen

    * Update selector in E2E tests

    * Rename files to follow kebab-case convention

diff --git a/plugins/woocommerce/changelog/add-58403 b/plugins/woocommerce/changelog/add-58403
new file mode 100644
index 0000000000..f90b0d1a70
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-58403
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Handpicked Products: journey starts with product picker
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
index 701c9be955..115bed117d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
@@ -2,7 +2,7 @@
  * External dependencies
  */
 import { store as blockEditorStore } from '@wordpress/block-editor';
-import { useState } from '@wordpress/element';
+import { useState, useEffect } from '@wordpress/element';
 import { useSelect } from '@wordpress/data';
 import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
 import { Spinner, Flex } from '@wordpress/components';
@@ -11,6 +11,7 @@ import { Spinner, Flex } from '@wordpress/components';
  * Internal dependencies
  */
 import {
+	CoreCollectionNames,
 	ProductCollectionContentProps,
 	ProductCollectionEditComponentProps,
 	ProductCollectionUIStatesInEditor,
@@ -19,7 +20,8 @@ import ProductCollectionPlaceholder from './product-collection-placeholder';
 import ProductCollectionContent from './product-collection-content';
 import CollectionSelectionModal from './collection-selection-modal';
 import { useProductCollectionUIState } from '../utils';
-import ProductPicker from './ProductPicker';
+import SingleProductPicker from './single-product-picker';
+import MultiProductPicker from './multi-product-picker';
 import { useTracksLocation } from '../tracks-utils';
 import { useRegisterEmailCollections } from '../hooks/use-register-email-collections';

@@ -32,6 +34,24 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
 	useRegisterEmailCollections();

 	const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
+
+	// Track if the hand-picked products picker is active.
+	// This allows multi-select before clicking "Done".
+	const [ isHandPickedPickerActive, setIsHandPickedPickerActive ] =
+		useState( false );
+
+	const isHandPickedCollection =
+		attributes.collection === CoreCollectionNames.HAND_PICKED;
+	const hasHandPickedProducts =
+		( attributes.query?.woocommerceHandPickedProducts?.length ?? 0 ) > 0;
+
+	// Activate the picker when Hand-Picked collection is selected with no products
+	useEffect( () => {
+		if ( isHandPickedCollection && ! hasHandPickedProducts ) {
+			setIsHandPickedPickerActive( true );
+		}
+	}, [ isHandPickedCollection, hasHandPickedProducts ] );
+
 	const hasInnerBlocks = useSelect(
 		( select ) =>
 			!! select( blockEditorStore ).getBlocks( clientId ).length,
@@ -65,6 +85,20 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
 	};

 	const renderComponent = () => {
+		// Show the hand-picked products picker if it's active (local state).
+		// This allows multi-select before clicking "Done".
+		// The inspector controls (HandPickedProductsControlField) are inside
+		// ProductCollectionContent, so they're automatically hidden while
+		// the picker is shown.
+		if ( isHandPickedCollection && isHandPickedPickerActive ) {
+			return (
+				<MultiProductPicker
+					{ ...props }
+					onDone={ () => setIsHandPickedPickerActive( false ) }
+				/>
+			);
+		}
+
 		switch ( productCollectionUIStateInEditor ) {
 			case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
 				return (
@@ -75,18 +109,27 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
 				);
 			case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
 				return (
-					<ProductPicker
+					<SingleProductPicker
 						{ ...props }
 						isDeletedProductReference={ false }
 					/>
 				);
 			case ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE:
 				return (
-					<ProductPicker
+					<SingleProductPicker
 						{ ...props }
 						isDeletedProductReference={ true }
 					/>
 				);
+			case ProductCollectionUIStatesInEditor.HAND_PICKED_PRODUCTS_PICKER:
+				// This case is hit when no products are selected
+				// and the picker was previously dismissed but products were removed
+				return (
+					<MultiProductPicker
+						{ ...props }
+						onDone={ () => setIsHandPickedPickerActive( false ) }
+					/>
+				);
 			case ProductCollectionUIStatesInEditor.VALID:
 			case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
 				return (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
index 34ab885334..8b7f55829f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
@@ -159,6 +159,21 @@ const ProductCollectionInspectorControls = (
 				location={ props.location }
 			/>

+			{
+				/**
+				 * "Hand-Picked" collection-specific control.
+				 * Placed at the top for easy access when editing product selection.
+				 * Only rendered when ProductCollectionContent is shown (not during picker).
+				 */
+				collection === CoreCollectionNames.HAND_PICKED && (
+					<PanelBody>
+						<HandPickedProductsControlField
+							{ ...queryControlProps }
+						/>
+					</PanelBody>
+				)
+			}
+
 			<ToolsPanel
 				label={ __( 'Settings', 'woocommerce' ) }
 				resetAll={ () => {
@@ -265,12 +280,14 @@ const isProductCollection = ( blockName: string ) =>
 const CollectionSpecificControls = (
 	props: ProductCollectionEditComponentProps
 ) => {
-	const { collection } = props.attributes;
+	const { attributes, context } = props;
+	const { collection } = attributes;
+
 	const setQueryAttributeBind = useMemo(
 		() => setQueryAttribute.bind( null, props ),
 		[ props ]
 	);
-	const tracksLocation = useTracksLocation( props.context.templateSlug );
+	const tracksLocation = useTracksLocation( context.templateSlug );
 	const trackInteraction = ( filter: FilterName ) => {
 		return recordEvent(
 			'blocks_product_collection_inspector_control_clicked',
@@ -284,7 +301,7 @@ const CollectionSpecificControls = (
 	const queryControlProps = {
 		setQueryAttribute: setQueryAttributeBind,
 		trackInteraction,
-		query: props.attributes.query,
+		query: attributes.query,
 	};

 	const isByTaxonomy =
@@ -294,18 +311,6 @@ const CollectionSpecificControls = (

 	return (
 		<InspectorControls>
-			{
-				/**
-				 * "Hand-Picked" collection-specific controls.
-				 */
-				collection === CoreCollectionNames.HAND_PICKED && (
-					<PanelBody>
-						<HandPickedProductsControlField
-							{ ...queryControlProps }
-						/>
-					</PanelBody>
-				)
-			}
 			{
 				/**
 				 * "Related Products" collection-specific controls.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/multi-product-picker.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/multi-product-picker.tsx
new file mode 100644
index 0000000000..a029a41839
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/multi-product-picker.tsx
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, info } from '@wordpress/icons';
+import ProductsControl from '@woocommerce/editor-components/products-control';
+import { Placeholder, Button } from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductCollectionEditComponentProps } from '../types';
+import { getCollectionByName } from '../collections';
+import { setQueryAttribute } from '../utils';
+
+interface MultiProductPickerProps extends ProductCollectionEditComponentProps {
+	onDone: () => void;
+}
+
+const MultiProductPicker = ( props: MultiProductPickerProps ) => {
+	const { attributes, onDone } = props;
+	const blockProps = useBlockProps();
+
+	const collection = getCollectionByName( attributes.collection );
+
+	// Convert string IDs to numbers for ProductsControl.
+	const selectedProductIds = (
+		attributes.query?.woocommerceHandPickedProducts || []
+	).map( Number );
+
+	const hasSelectedProducts = selectedProductIds.length > 0;
+
+	if ( ! collection ) {
+		return null;
+	}
+
+	return (
+		<div { ...blockProps }>
+			<Placeholder className="wc-block-editor-product-collection__product-picker">
+				<div className="wc-block-editor-product-collection__product-picker-info">
+					{ /* @ts-expect-error Icon types are incomplete */ }
+					<Icon
+						icon={ info }
+						className="wc-block-editor-product-collection__info-icon"
+					/>
+					<span>
+						{ __(
+							'Select products to display in this collection.',
+							'woocommerce'
+						) }
+					</span>
+				</div>
+				<div className="wc-block-editor-product-collection__product-picker-selection">
+					{ /* @ts-expect-error Props provided by withSearchedProducts HOC */ }
+					<ProductsControl
+						selected={ selectedProductIds }
+						onChange={ ( value = [] ) => {
+							const ids = value.map( ( { id }: { id: number } ) =>
+								String( id )
+							);
+							setQueryAttribute( props, {
+								woocommerceHandPickedProducts: ids,
+							} );
+						} }
+					/>
+					<Button
+						variant="primary"
+						onClick={ onDone }
+						disabled={ ! hasSelectedProducts }
+					>
+						{ __( 'Done', 'woocommerce' ) }
+					</Button>
+				</div>
+			</Placeholder>
+		</div>
+	);
+};
+
+export default MultiProductPicker;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/single-product-picker.tsx
similarity index 90%
rename from plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx
rename to plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/single-product-picker.tsx
index e04e48741a..5a39de6a2d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/ProductPicker.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/single-product-picker.tsx
@@ -19,7 +19,7 @@ import { useBlockProps } from '@wordpress/block-editor';
 import type { ProductCollectionEditComponentProps } from '../types';
 import { getCollectionByName } from '../collections';

-const ProductPicker = (
+const SingleProductPicker = (
 	props: ProductCollectionEditComponentProps & {
 		isDeletedProductReference: boolean;
 	}
@@ -52,11 +52,11 @@ const ProductPicker = (

 	return (
 		<div { ...blockProps }>
-			<Placeholder className="wc-blocks-product-collection__editor-product-picker">
+			<Placeholder className="wc-block-editor-product-collection__product-picker">
 				<HStack alignment="center">
 					<Icon
 						icon={ info }
-						className="wc-blocks-product-collection__info-icon"
+						className="wc-block-editor-product-collection__info-icon"
 					/>
 					<Text>{ infoText }</Text>
 				</HStack>
@@ -84,4 +84,4 @@ const ProductPicker = (
 	);
 };

-export default ProductPicker;
+export default SingleProductPicker;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/editor.scss
index 06c87fbfb7..86ea6382c3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/editor.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/editor.scss
@@ -192,14 +192,29 @@ $max-button-width: calc(100% / #{$max-button-columns});
 }

 // Editor Product Picker
-.wc-blocks-product-collection__editor-product-picker {
-	.wc-blocks-product-collection__info-icon {
+.wc-block-editor-product-collection__product-picker {
+	.wc-block-editor-product-collection__info-icon {
 		fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
 	}

 	.woocommerce-search-list__search {
 		margin: 0;
 	}
+
+	.wc-block-editor-product-collection__product-picker-info {
+		display: flex;
+		align-items: center;
+		gap: $gap-small;
+	}
+
+	.wc-block-editor-product-collection__product-picker-selection {
+		width: 100%;
+	}
+}
+
+// Editor Taxonomy Picker
+.wc-block-editor-product-collection__taxonomy-picker-selection {
+	width: 100%;
 }

 // Product to Show Radio Control
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
index 655f5e116d..e8fab2ec71 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
@@ -12,6 +12,7 @@ import { WooCommerceBlockLocation } from '../product-template/utils';
 export enum ProductCollectionUIStatesInEditor {
 	COLLECTION_PICKER = 'collection_chooser',
 	PRODUCT_REFERENCE_PICKER = 'product_context_picker',
+	HAND_PICKED_PRODUCTS_PICKER = 'hand_picked_products_picker',
 	VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
 	VALID = 'valid',
 	DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
index b2e46209c1..6c270f3462 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
@@ -34,6 +34,7 @@ import {
 	PreviewState,
 	SetPreviewState,
 	ProductCollectionUIStatesInEditor,
+	CoreCollectionNames,
 } from './types';
 import {
 	coreQueryPaginationBlockName,
@@ -265,7 +266,26 @@ export const useProductCollectionUIState = ( {
 		}

 		/**
-		 * Case 3: Preview mode - based on `usesReference` value
+		 * Case 3: Hand-picked products picker
+		 * Show the product picker when the Hand-Picked collection is selected
+		 * but no products have been chosen yet.
+		 */
+		const isHandPickedCollection =
+			attributes.collection === CoreCollectionNames.HAND_PICKED;
+		const hasHandPickedProducts =
+			( attributes.query?.woocommerceHandPickedProducts?.length ?? 0 ) >
+			0;
+
+		if (
+			isCollectionSelected &&
+			isHandPickedCollection &&
+			! hasHandPickedProducts
+		) {
+			return ProductCollectionUIStatesInEditor.HAND_PICKED_PRODUCTS_PICKER;
+		}
+
+		/**
+		 * Case 4: Preview mode - based on `usesReference` value
 		 */
 		if ( isInRequiredLocation ) {
 			/**
@@ -310,6 +330,7 @@ export const useProductCollectionUIState = ( {
 		product,
 		hasInnerBlocks,
 		attributes.query?.productReference,
+		attributes.query?.woocommerceHandPickedProducts,
 	] );

 	return { productCollectionUIStateInEditor, isLoading: ! hasResolved };
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
index 2b24d74ea6..1d16de918f 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
@@ -56,7 +56,7 @@ export const SELECTORS = {
 	previewButtonTestID: 'product-collection-preview-button',
 	collectionPlaceholder:
 		'[data-type="woocommerce/product-collection"] .components-placeholder',
-	productPicker: '.wc-blocks-product-collection__editor-product-picker',
+	productPicker: '.wc-block-editor-product-collection__product-picker',
 	linkedProductControl: {
 		button: '.wc-block-product-collection-linked-product-control__button',
 		popoverContent: