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";