Commit 7eb3ad9b475 for woocommerce

commit 7eb3ad9b4754716150d8e495f7ad27979bedb3c9
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Jun 3 10:02:51 2026 +0200

    Convert ProductsControl component to TS (#65376)

    * Rename file

    * Convert ProductsControl component to TS

    * Add changelog

    * Reorder imports

    * Further improve types

    * Fix PHP warning in Product Collection unselected products state

    * Avoid defaulting to ID=0

    * Fix PHP warning in Product Collection unselected products state (II)

    * chore: trim ProductsControl changelog

    ---------

    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

diff --git a/plugins/woocommerce/changelog/update-products-control-ts b/plugins/woocommerce/changelog/update-products-control-ts
new file mode 100644
index 00000000000..7dacd81e157
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-products-control-ts
@@ -0,0 +1,3 @@
+Significance: patch
+Type: update
+Comment: Convert ProductsControl component to TS
diff --git a/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.js b/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.js
deleted file mode 100644
index a5d6a59ac82..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * External dependencies
- */
-import { __, _n, sprintf } from '@wordpress/i18n';
-import { SearchListControl } from '@woocommerce/editor-components/search-list-control';
-import PropTypes from 'prop-types';
-import { withSearchedProducts } from '@woocommerce/block-hocs';
-import ErrorMessage from '@woocommerce/editor-components/error-placeholder/error-message';
-import { decodeEntities } from '@wordpress/html-entities';
-
-/**
- * The products control exposes a custom selector for searching and selecting
- * products.
- *
- * @param {Object}   props           Component props.
- * @param {string}   props.error
- * @param {Function} props.onChange  Callback fired when the selected item changes
- * @param {Function} props.onSearch  Callback fired when a search is triggered
- * @param {Array}    props.selected  An array of selected products.
- * @param {Array}    props.products  An array of products to select from.
- * @param {boolean}  props.isLoading Whether or not the products are being loaded.
- * @param {boolean}  props.isCompact Whether or not the control should have compact styles.
- *
- * @return {Function} A functional component.
- */
-const ProductsControl = ( {
-	error,
-	onChange,
-	onSearch,
-	selected = [],
-	products = [],
-	isLoading = true,
-	isCompact = false,
-} ) => {
-	const messages = {
-		clear: __( 'Clear all products', 'woocommerce' ),
-		list: __( 'Products', 'woocommerce' ),
-		noItems: __( "Your store doesn't have any products.", 'woocommerce' ),
-		search: __( 'Search for products to display', 'woocommerce' ),
-		selected: ( n ) =>
-			sprintf(
-				/* translators: %d is the number of selected products. */
-				_n(
-					'%d product selected',
-					'%d products selected',
-					n,
-					'woocommerce'
-				),
-				n
-			),
-		updated: __( 'Product search results updated.', 'woocommerce' ),
-	};
-
-	if ( error ) {
-		return <ErrorMessage error={ error } />;
-	}
-
-	return (
-		<SearchListControl
-			className="woocommerce-products"
-			list={ products.map( ( product ) => {
-				const formattedSku = product.sku
-					? ' (' + product.sku + ')'
-					: '';
-				return {
-					...product,
-					name: `${ decodeEntities(
-						product.name
-					) }${ formattedSku }`,
-				};
-			} ) }
-			isCompact={ isCompact }
-			isLoading={ isLoading }
-			selected={ products.filter( ( { id } ) =>
-				selected.includes( id )
-			) }
-			onSearch={ onSearch }
-			onChange={ onChange }
-			messages={ messages }
-		/>
-	);
-};
-
-ProductsControl.propTypes = {
-	onChange: PropTypes.func.isRequired,
-	onSearch: PropTypes.func,
-	selected: PropTypes.array,
-	products: PropTypes.array,
-	isCompact: PropTypes.bool,
-	isLoading: PropTypes.bool,
-};
-
-export default withSearchedProducts( ProductsControl );
diff --git a/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.tsx b/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.tsx
new file mode 100644
index 00000000000..7775ffb3208
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/editor-components/products-control/index.tsx
@@ -0,0 +1,99 @@
+/**
+ * External dependencies
+ */
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { SearchListControl } from '@woocommerce/editor-components/search-list-control';
+import { withSearchedProducts } from '@woocommerce/block-hocs';
+import ErrorMessage from '@woocommerce/editor-components/error-placeholder/error-message';
+import { decodeEntities } from '@wordpress/html-entities';
+import { convertProductResponseItemToSearchItem } from '@woocommerce/utils';
+import type { ProductResponseItem } from '@woocommerce/types';
+import type { ErrorObject } from '@woocommerce/editor-components/error-placeholder';
+import type {
+	SearchListItem,
+	SearchListMessages,
+} from '@woocommerce/editor-components/search-list-control/types';
+import type { ComponentType } from 'react';
+
+interface ProductsControlProps {
+	error: ErrorObject | null;
+	isLoading?: boolean;
+	onSearch?: ( search: string ) => void;
+	products?: ProductResponseItem[];
+	selected?: number[];
+	onChange: ( value: SearchListItem< ProductResponseItem >[] ) => void;
+	isCompact?: boolean;
+}
+
+const ProductsControl = ( {
+	error,
+	onChange,
+	onSearch,
+	selected = [],
+	products = [],
+	isLoading = true,
+	isCompact = false,
+}: ProductsControlProps ): JSX.Element => {
+	const messages: Partial< SearchListMessages > = {
+		clear: __( 'Clear all products', 'woocommerce' ),
+		noItems: __( "Your store doesn't have any products.", 'woocommerce' ),
+		search: __( 'Search for products to display', 'woocommerce' ),
+		selected: ( n: number ) =>
+			sprintf(
+				/* translators: %d is the number of selected products. */
+				_n(
+					'%d product selected',
+					'%d products selected',
+					n,
+					'woocommerce'
+				),
+				n
+			),
+		updated: __( 'Product search results updated.', 'woocommerce' ),
+	};
+
+	if ( error ) {
+		return <ErrorMessage error={ error } />;
+	}
+
+	const productList = products.map( convertProductResponseItemToSearchItem );
+
+	return (
+		<SearchListControl
+			className="woocommerce-products"
+			list={ productList.map( ( product ) => {
+				const formattedSku = product.details?.sku
+					? ' (' + product.details.sku + ')'
+					: '';
+				return {
+					...product,
+					name: `${ decodeEntities(
+						product.name
+					) }${ formattedSku }`,
+				};
+			} ) }
+			isCompact={ isCompact }
+			isLoading={ isLoading }
+			isSingle={ false }
+			selected={ productList.filter( ( { details } ) => {
+				if ( ! details || ! Number.isSafeInteger( details.id ) ) {
+					return false;
+				}
+				return selected.includes( details.id );
+			} ) }
+			onSearch={ onSearch }
+			onChange={ onChange }
+			messages={ messages }
+		/>
+	);
+};
+
+const WrappedProductsControl: ComponentType< {
+	onChange: ( value: SearchListItem< ProductResponseItem >[] ) => void;
+	selected: number[];
+	isCompact?: boolean;
+} > =
+	// @ts-expect-error HOC typing for injected products is narrower than this control's search list item shape.
+	withSearchedProducts( ProductsControl );
+
+export default WrappedProductsControl;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
index 2b8a1f2c3aa..2e34b826e15 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
@@ -150,7 +150,7 @@ class Renderer {
 				if ( $is_enhanced_pagination_enabled && isset( $this->parsed_block ) ) {
 					$p->set_attribute(
 						'data-wp-router-region',
-						'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
+						'wc-product-collection-' . ( $this->parsed_block['attrs']['queryId'] ?? '0' )
 					);
 				}
 			}