Commit 83fa84e0842 for woocommerce
commit 83fa84e084251d6cd9906a9f32e4db2c44664595
Author: verofasulo <98944206+verofasulo@users.noreply.github.com>
Date: Thu May 14 10:01:33 2026 +0200
Add Shipping Class filter to the experimental products app product list (#64823)
* Add Shipping Class filter to the experimental products app product list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix shipping_class field getValue to return term ID so the filter aligns with row state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Luigi Teschio <gigitux@gmail.com>
diff --git a/packages/js/experimental-products-app/changelog/add-products-app-shipping-class-filter b/packages/js/experimental-products-app/changelog/add-products-app-shipping-class-filter
new file mode 100644
index 00000000000..f589d907a18
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-products-app-shipping-class-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a Shipping Class filter to the experimental products app product list.
diff --git a/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx b/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx
index 1533fecc6ab..4b632bba176 100644
--- a/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx
@@ -2,7 +2,9 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { useSelect } from '@wordpress/data';
+import { resolveSelect, useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl } from '@wordpress/ui';
import type { Field } from '@wordpress/dataviews';
@@ -25,7 +27,9 @@ const fieldDefinition = {
label: __( 'Shipping Class', 'woocommerce' ),
enableSorting: false,
enableHiding: false,
- filterBy: false,
+ filterBy: {
+ operators: [ 'isAny', 'isNone' ],
+ },
} satisfies Partial< Field< ProductEntityRecord > >;
export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
@@ -34,8 +38,21 @@ export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
label: __( 'Shipping Class', 'woocommerce' ),
enableSorting: false,
type: 'text',
+ getValue: ( { item } ) =>
+ item.shipping_class_id ? item.shipping_class_id.toString() : '',
+ render: ( { item } ) => item.shipping_class ?? '',
+ getElements: async () => {
+ const records = ( await resolveSelect( coreStore ).getEntityRecords(
+ 'taxonomy',
+ 'product_shipping_class',
+ { per_page: -1 }
+ ) ) as Array< { id: number; name: string } > | null;
+ return ( records ?? [] ).map( ( { id, name } ) => ( {
+ value: id.toString(),
+ label: decodeEntities( name ),
+ } ) );
+ },
isVisible: ( item ) => ! item.virtual,
- getValue: ( { item } ) => item.shipping_class,
Edit: ( { data, onChange, field } ) => {
const { shippingClasses } = useSelect( ( select ) => {
// TODO: Register shipping class entity and use it instead.
diff --git a/packages/js/experimental-products-app/src/product-list/query.test.ts b/packages/js/experimental-products-app/src/product-list/query.test.ts
index 90f3675b94c..7a03de36045 100644
--- a/packages/js/experimental-products-app/src/product-list/query.test.ts
+++ b/packages/js/experimental-products-app/src/product-list/query.test.ts
@@ -203,4 +203,33 @@ describe( 'buildProductListQuery', () => {
expect( query.brand ).toEqual( '8,9' );
} );
+ it( 'maps the shipping_class isAny filter to the shipping_class query param', () => {
+ const query = buildProductListQuery( {
+ ...baseView,
+ filters: [
+ {
+ field: 'shipping_class',
+ operator: 'isAny',
+ value: [ '3', 4 ],
+ },
+ ],
+ } as View );
+
+ expect( query.shipping_class ).toEqual( '3,4' );
+ } );
+
+ it( 'maps the shipping_class isNone filter to exclude_shipping_class', () => {
+ const query = buildProductListQuery( {
+ ...baseView,
+ filters: [
+ {
+ field: 'shipping_class',
+ operator: 'isNone',
+ value: [ '3', 4 ],
+ },
+ ],
+ } as View );
+
+ expect( query.exclude_shipping_class ).toEqual( [ 3, 4 ] );
+ } );
} );
diff --git a/packages/js/experimental-products-app/src/product-list/query.ts b/packages/js/experimental-products-app/src/product-list/query.ts
index 6fce94a14c8..274637a0cb4 100644
--- a/packages/js/experimental-products-app/src/product-list/query.ts
+++ b/packages/js/experimental-products-app/src/product-list/query.ts
@@ -16,6 +16,7 @@ export type ProductListQuery = Omit< ProductQuery, 'status' > & {
include_types?: ProductType[];
exclude_types?: ProductType[];
exclude_category?: number[];
+ exclude_shipping_class?: number[];
exclude_tag?: number[];
min_stock_quantity?: string;
max_stock_quantity?: string;
@@ -138,6 +139,21 @@ function applyBrandFilter( query: ProductListQuery, filter: Filter ) {
query.brand = values.join( ',' );
}
+function applyShippingClassFilter( query: ProductListQuery, filter: Filter ) {
+ const values = getNumericValues( filter.value );
+
+ if ( values.length === 0 ) {
+ return;
+ }
+
+ if ( filter.operator === 'isNone' ) {
+ query.exclude_shipping_class = values;
+ return;
+ }
+
+ query.shipping_class = values.join( ',' );
+}
+
function applyStockFilter( query: ProductListQuery, filter: Filter ) {
const [ stockStatus ] = getStringValues( filter.value );
@@ -200,6 +216,9 @@ export function buildProductListQuery( view: View ): ProductListQuery {
case 'categories':
applyCategoryFilter( query, filter );
break;
+ case 'shipping_class':
+ applyShippingClassFilter( query, filter );
+ break;
case 'tags':
applyTagFilter( query, filter );
break;