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' )
);
}
}