Commit 8e4087110bb for woocommerce

commit 8e4087110bb69a5eb9c424ced2826f77efadeb8e
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Thu May 21 09:57:17 2026 +0200

    Fix product list filter empty state (#65219)

    * Fix product list filter empty state

    * Add changelog entry for product list empty state

    * fix import

diff --git a/packages/js/experimental-products-app/changelog/fix-products-dashboard-filter-empty-state b/packages/js/experimental-products-app/changelog/fix-products-dashboard-filter-empty-state
new file mode 100644
index 00000000000..8811e09dd3f
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/fix-products-dashboard-filter-empty-state
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show a search and filter empty state when product filters return no results.
diff --git a/packages/js/experimental-products-app/src/product-list/empty-state/index.tsx b/packages/js/experimental-products-app/src/product-list/empty-state/index.tsx
index baa3982e32c..1307071df42 100644
--- a/packages/js/experimental-products-app/src/product-list/empty-state/index.tsx
+++ b/packages/js/experimental-products-app/src/product-list/empty-state/index.tsx
@@ -2,7 +2,7 @@
  * External dependencies
  */
 import { __ } from '@wordpress/i18n';
-import { EmptyState } from '@wordpress/ui';
+import { Button, EmptyState } from '@wordpress/ui';

 /**
  * Internal dependencies
@@ -15,7 +15,20 @@ type EmptyStateCopy = {
 	description: string;
 };

-function getEmptyStateCopy( tab: StatusTab ): EmptyStateCopy {
+function getEmptyStateCopy(
+	tab: StatusTab,
+	isSearchOrFilterResult: boolean
+): EmptyStateCopy {
+	if ( isSearchOrFilterResult ) {
+		return {
+			title: __( 'No products match your filters', 'woocommerce' ),
+			description: __(
+				'Try clearing some filters or adjusting your search query.',
+				'woocommerce'
+			),
+		};
+	}
+
 	switch ( tab ) {
 		case 'publish':
 			return {
@@ -62,11 +75,20 @@ function getEmptyStateCopy( tab: StatusTab ): EmptyStateCopy {
 }

 type ProductListEmptyStateProps = {
+	isSearchOrFilterResult?: boolean;
+	onClearFilters?: () => void;
 	tab: StatusTab;
 };

-export function ProductListEmptyState( { tab }: ProductListEmptyStateProps ) {
-	const { title, description } = getEmptyStateCopy( tab );
+export function ProductListEmptyState( {
+	isSearchOrFilterResult = false,
+	onClearFilters,
+	tab,
+}: ProductListEmptyStateProps ) {
+	const { title, description } = getEmptyStateCopy(
+		tab,
+		isSearchOrFilterResult
+	);

 	return (
 		<EmptyState.Root className="woocommerce-product-list__empty-state">
@@ -74,7 +96,16 @@ export function ProductListEmptyState( { tab }: ProductListEmptyStateProps ) {
 				<ProductListEmptyStateIcon />
 			</EmptyState.Visual>
 			<EmptyState.Title>{ title }</EmptyState.Title>
-			<EmptyState.Description>{ description }</EmptyState.Description>
+			<EmptyState.Description className="woocommerce-product-list__empty-state-description">
+				{ description }
+			</EmptyState.Description>
+			{ isSearchOrFilterResult && onClearFilters && (
+				<EmptyState.Actions>
+					<Button variant="outline" onClick={ onClearFilters }>
+						{ __( 'Clear filters', 'woocommerce' ) }
+					</Button>
+				</EmptyState.Actions>
+			) }
 		</EmptyState.Root>
 	);
 }
diff --git a/packages/js/experimental-products-app/src/product-list/empty-state/style.scss b/packages/js/experimental-products-app/src/product-list/empty-state/style.scss
new file mode 100644
index 00000000000..bf660a4f95f
--- /dev/null
+++ b/packages/js/experimental-products-app/src/product-list/empty-state/style.scss
@@ -0,0 +1,3 @@
+.woocommerce-product-list__empty-state-description {
+	width: max-content;
+}
diff --git a/packages/js/experimental-products-app/src/product-list/index.tsx b/packages/js/experimental-products-app/src/product-list/index.tsx
index 064c6286a46..faf6031f230 100644
--- a/packages/js/experimental-products-app/src/product-list/index.tsx
+++ b/packages/js/experimental-products-app/src/product-list/index.tsx
@@ -32,6 +32,7 @@ import {
 	getProductListTab,
 	getProductsWithEmbeddedVariations,
 	getSelectionFromPostId,
+	hasActiveProductListSearchOrFilters,
 	isProductEditorAccessible,
 } from './utils';
 import { useProductActions } from '../dataviews-actions';
@@ -150,6 +151,18 @@ export default function ProductList( {
 		() => getProductsWithEmbeddedVariations( records || EMPTY_ARRAY ),
 		[ records ]
 	);
+	const hasActiveSearchOrFilters =
+		hasActiveProductListSearchOrFilters( view );
+
+	const onClearSearchOrFilters = useCallback( () => {
+		setView( {
+			...view,
+			filters: [],
+			page: 1,
+			search: '',
+		} );
+	}, [ setView, view ] );
+
 	const getItemParentId = useCallback(
 		( item: ProductEntityRecord ) =>
 			item.parent_id && item.parent_id > 0 ? item.parent_id : undefined,
@@ -294,7 +307,13 @@ export default function ProductList( {
 				selection={ selection }
 				defaultLayouts={ DEFAULT_LAYOUTS }
 				isItemClickable={ isProductEditorAccessible }
-				empty={ <ProductListEmptyState tab={ selectedTab } /> }
+				empty={
+					<ProductListEmptyState
+						isSearchOrFilterResult={ hasActiveSearchOrFilters }
+						onClearFilters={ onClearSearchOrFilters }
+						tab={ selectedTab }
+					/>
+				}
 				renderItemLink={ ( { item, ...props } ) => (
 					<a
 						{ ...props }
diff --git a/packages/js/experimental-products-app/src/product-list/utils.test.ts b/packages/js/experimental-products-app/src/product-list/utils.test.ts
index 0e209dccadb..a75e665018e 100644
--- a/packages/js/experimental-products-app/src/product-list/utils.test.ts
+++ b/packages/js/experimental-products-app/src/product-list/utils.test.ts
@@ -1,8 +1,14 @@
+/**
+ * External dependencies
+ */
+import type { View } from '@wordpress/dataviews';
+
 /**
  * Internal dependencies
  */
 import type { ProductEntityRecord } from '../fields/types';
 import {
+	hasActiveProductListSearchOrFilters,
 	getProductListNavigationPath,
 	getProductsWithEmbeddedVariations,
 } from './utils';
@@ -82,4 +88,58 @@ describe( 'product list utils', () => {
 			).toEqual( [ parent, listedVariation ] );
 		} );
 	} );
+
+	describe( 'hasActiveProductListSearchOrFilters', () => {
+		const baseView = {
+			type: 'table',
+			page: 1,
+			perPage: 20,
+			filters: [],
+		} as View;
+
+		it( 'returns true when the view has a search query', () => {
+			expect(
+				hasActiveProductListSearchOrFilters( {
+					...baseView,
+					search: ' hoodie ',
+				} )
+			).toBe( true );
+		} );
+
+		it( 'returns true when the view has a filter value', () => {
+			expect(
+				hasActiveProductListSearchOrFilters( {
+					...baseView,
+					filters: [
+						{
+							field: 'stock_quantity',
+							operator: 'is',
+							value: 0,
+						},
+					],
+				} as View )
+			).toBe( true );
+		} );
+
+		it( 'ignores empty search and filter values', () => {
+			expect(
+				hasActiveProductListSearchOrFilters( {
+					...baseView,
+					search: ' ',
+					filters: [
+						{
+							field: 'categories',
+							operator: 'isAny',
+							value: [],
+						},
+						{
+							field: 'tags',
+							operator: 'isAny',
+							value: [ '', undefined ],
+						},
+					],
+				} as View )
+			).toBe( false );
+		} );
+	} );
 } );
diff --git a/packages/js/experimental-products-app/src/product-list/utils.ts b/packages/js/experimental-products-app/src/product-list/utils.ts
index d88c76906ec..189d0e38a8c 100644
--- a/packages/js/experimental-products-app/src/product-list/utils.ts
+++ b/packages/js/experimental-products-app/src/product-list/utils.ts
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import { addQueryArgs, getQueryArgs } from '@wordpress/url';
+import { Filter, View } from '@wordpress/dataviews';

 /**
  * Internal dependencies
@@ -94,3 +95,23 @@ export function getSelectionFromPostId( postId?: string ) {
 export function isProductEditorAccessible( item: ProductEntityRecord ) {
 	return item.status !== 'trash';
 }
+
+function hasFilterValue( value: Filter[ 'value' ] ): boolean {
+	if ( Array.isArray( value ) ) {
+		return value.some( hasFilterValue );
+	}
+
+	if ( typeof value === 'string' ) {
+		return value.trim() !== '';
+	}
+
+	return value !== undefined && value !== null;
+}
+
+export function hasActiveProductListSearchOrFilters( view: View ) {
+	return (
+		( typeof view.search === 'string' && view.search.trim() !== '' ) ||
+		view.filters?.some( ( filter ) => hasFilterValue( filter.value ) ) ===
+			true
+	);
+}
diff --git a/packages/js/experimental-products-app/src/style.scss b/packages/js/experimental-products-app/src/style.scss
index 26c0a3935bd..811d86ed363 100644
--- a/packages/js/experimental-products-app/src/style.scss
+++ b/packages/js/experimental-products-app/src/style.scss
@@ -204,4 +204,5 @@
 	gap: 8px;
 }

+@import "./product-list/empty-state/style.scss";
 @import "./variation-view/style.scss";