Commit 0c2c10532ca for woocommerce
commit 0c2c10532ca3eb16a94e9d4a265af95107408602
Author: Luigi Teschio <gigitux@gmail.com>
Date: Thu May 7 10:47:58 2026 +0200
Fix quick edit fields for product types (#64672)
* Fix quick edit field visibility by product type
* Add changelog entry for quick edit visibility fix
* fix field
diff --git a/packages/js/experimental-products-app/changelog/fix-product-field-visibility b/packages/js/experimental-products-app/changelog/fix-product-field-visibility
new file mode 100644
index 00000000000..9dee81f6d26
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/fix-product-field-visibility
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Limit quick edit fields to compatible product types
diff --git a/packages/js/experimental-products-app/src/fields/downloadable/field.tsx b/packages/js/experimental-products-app/src/fields/downloadable/field.tsx
index ac38d9d4517..3774bad3711 100644
--- a/packages/js/experimental-products-app/src/fields/downloadable/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/downloadable/field.tsx
@@ -37,6 +37,7 @@ const fieldDefinition = {
export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
...fieldDefinition,
type: 'boolean',
+ isVisible: ( item ) => item.downloadable === true,
getValue: ( { item } ) => item.downloadable,
Edit: ( { data, onChange } ) => {
const downloads = ( data.downloads ?? [] ) as Array<
diff --git a/packages/js/experimental-products-app/src/product-edit/index.tsx b/packages/js/experimental-products-app/src/product-edit/index.tsx
index dcf147e13e3..1efe2771e73 100644
--- a/packages/js/experimental-products-app/src/product-edit/index.tsx
+++ b/packages/js/experimental-products-app/src/product-edit/index.tsx
@@ -87,7 +87,7 @@ function ProductEditForm( {
const mergedData = buildMergedProductEditData( selectedProducts );
const visibleFields = getVisibleProductEditFields(
editableFields,
- mergedData
+ selectedProducts
);
const form = {
diff --git a/packages/js/experimental-products-app/src/product-edit/utils.test.ts b/packages/js/experimental-products-app/src/product-edit/utils.test.ts
index a8c2287d7f9..d3233b03270 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.test.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.test.ts
@@ -7,7 +7,22 @@ import type { Field } from '@wordpress/dataviews';
* Internal dependencies
*/
import type { ProductEntityRecord } from '../fields/types';
-import { buildMergedProductEditData, getProductEditFields } from './utils';
+import { productFields } from '../product-list/fields';
+import {
+ buildMergedProductEditData,
+ EXCLUDED_PRODUCT_EDIT_FIELD_IDS,
+ getProductEditFields,
+ getVisibleProductEditFields,
+} from './utils';
+
+jest.mock( '@woocommerce/settings', () => ( {
+ CURRENCY: {
+ code: 'USD',
+ symbol: '$',
+ symbolPosition: 'left',
+ precision: 2,
+ },
+} ) );
describe( 'product edit utils', () => {
const buildProduct = (
@@ -17,6 +32,10 @@ describe( 'product edit utils', () => {
id: 12,
name: 'Beanie',
status: 'draft',
+ type: 'simple',
+ virtual: false,
+ downloadable: false,
+ on_sale: false,
categories: [],
tags: [],
images: [],
@@ -103,4 +122,224 @@ describe( 'product edit utils', () => {
] )
);
} );
+
+ describe( 'getVisibleProductEditFields', () => {
+ const getVisibleFieldIds = ( products: ProductEntityRecord[] ) =>
+ getVisibleProductEditFields(
+ getProductEditFields( productFields ),
+ products
+ ).map( ( field ) => field.id );
+
+ const getVisibleField = (
+ products: ProductEntityRecord[],
+ fieldId: string
+ ) =>
+ getVisibleProductEditFields(
+ getProductEditFields( productFields ),
+ products
+ ).find( ( field ) => field.id === fieldId );
+
+ const expectFieldsHidden = (
+ fieldIds: string[],
+ hiddenFieldIds: string[]
+ ) => {
+ hiddenFieldIds.forEach( ( fieldId ) => {
+ expect( fieldIds ).not.toContain( fieldId );
+ } );
+ };
+
+ it( 'shows pricing, shipping, and linked product fields for simple physical products', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ type: 'simple',
+ virtual: false,
+ downloadable: false,
+ on_sale: true,
+ sale_price: '12',
+ date_on_sale_from: '2026-05-06T00:00:00',
+ } ),
+ ] );
+
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ 'upsell_ids',
+ 'cross_sell_ids',
+ ] )
+ );
+ expectFieldsHidden( fieldIds, [ 'external_url', 'button_text' ] );
+ } );
+
+ it( 'does not include excluded fields in product type compatibility', () => {
+ const fieldIds = getVisibleProductEditFields( productFields, [
+ buildProduct( {
+ type: 'simple',
+ } ),
+ ] ).map( ( field ) => field.id );
+
+ expect( fieldIds ).not.toEqual(
+ expect.arrayContaining( [ ...EXCLUDED_PRODUCT_EDIT_FIELD_IDS ] )
+ );
+ } );
+
+ it( 'uses the same compatible fields for simple products regardless of virtual status', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ type: 'simple',
+ virtual: true,
+ downloadable: false,
+ } ),
+ ] );
+
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [
+ 'price',
+ 'regular_price',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ 'upsell_ids',
+ 'cross_sell_ids',
+ ] )
+ );
+ expectFieldsHidden( fieldIds, [ 'external_url', 'button_text' ] );
+ } );
+
+ it( 'shows downloads for simple downloadable products', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ type: 'simple',
+ virtual: false,
+ downloadable: true,
+ } ),
+ ] );
+
+ expect( fieldIds ).toContain( 'downloadable' );
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ ] )
+ );
+ } );
+
+ it( 'shows external fields for external products', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ type: 'external',
+ } ),
+ ] );
+
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [
+ 'price',
+ 'regular_price',
+ 'external_url',
+ 'button_text',
+ 'upsell_ids',
+ ] )
+ );
+ expectFieldsHidden( fieldIds, [
+ 'cross_sell_ids',
+ 'downloadable',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ ] );
+ } );
+
+ it( 'hides parent pricing, downloads, and shipping fields for variable products', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ type: 'variable',
+ } ),
+ ] );
+
+ expectFieldsHidden( fieldIds, [
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
+ 'downloadable',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ ] );
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [ 'upsell_ids', 'cross_sell_ids' ] )
+ );
+ } );
+
+ it( 'hides a field in bulk edit if any selected product does not support it', () => {
+ const fieldIds = getVisibleFieldIds( [
+ buildProduct( {
+ id: 1,
+ type: 'simple',
+ virtual: false,
+ downloadable: false,
+ } ),
+ buildProduct( {
+ id: 2,
+ type: 'variable',
+ } ),
+ ] );
+
+ expectFieldsHidden( fieldIds, [
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ ] );
+ expect( fieldIds ).toEqual(
+ expect.arrayContaining( [ 'upsell_ids', 'cross_sell_ids' ] )
+ );
+ } );
+
+ it( 'does not return visibility predicates after checking selected products', () => {
+ const field = getVisibleField(
+ [
+ buildProduct( {
+ id: 1,
+ type: 'simple',
+ on_sale: true,
+ } ),
+ buildProduct( {
+ id: 2,
+ type: 'simple',
+ sale_price: '12',
+ } ),
+ ],
+ 'sale_price'
+ );
+
+ expect( field ).toBeDefined();
+ expect( field?.isVisible ).toBeUndefined();
+ } );
+ } );
} );
diff --git a/packages/js/experimental-products-app/src/product-edit/utils.ts b/packages/js/experimental-products-app/src/product-edit/utils.ts
index 13f33abfcff..190dda36420 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -24,6 +24,121 @@ const EXCLUDED_PRODUCT_EDIT_FIELD_ID_SET = new Set(
);
type ProductField = Field< ProductEntityRecord >;
+type ProductEditFieldId = ( typeof PRODUCT_EDIT_FIELD_IDS )[ number ];
+
+const PRODUCT_EDIT_FIELD_IDS = [
+ 'name',
+ 'short_description',
+ 'description',
+ 'images',
+ 'images_count',
+ 'product_status',
+ 'sku',
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
+ 'price_summary',
+ 'stock',
+ 'stock_quantity',
+ 'manage_stock',
+ 'inventory_summary',
+ 'categories',
+ 'tags',
+ 'organization_summary',
+ 'type',
+ 'featured',
+ 'catalog_visibility',
+ 'visibility_summary',
+ 'downloadable',
+ 'downloadable_count',
+ 'external_url',
+ 'button_text',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ 'shipping_summary',
+ 'tax_status',
+ 'upsell_ids',
+ 'cross_sell_ids',
+ 'linked_products_count',
+] as const;
+
+const COMMON_PRODUCT_EDIT_FIELD_IDS = [
+ 'name',
+ 'short_description',
+ 'description',
+ 'images',
+ 'product_status',
+ 'sku',
+ 'categories',
+ 'tags',
+ 'type',
+ 'featured',
+ 'catalog_visibility',
+ 'upsell_ids',
+] satisfies ProductEditFieldId[];
+
+const SIMPLE_PRODUCT_EDIT_FIELD_IDS = [
+ ...COMMON_PRODUCT_EDIT_FIELD_IDS,
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
+ 'stock',
+ 'stock_quantity',
+ 'manage_stock',
+ 'downloadable',
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
+ 'tax_status',
+ 'cross_sell_ids',
+] satisfies ProductEditFieldId[];
+
+const VARIABLE_PRODUCT_EDIT_FIELD_IDS = [
+ ...COMMON_PRODUCT_EDIT_FIELD_IDS,
+ 'stock',
+ 'stock_quantity',
+ 'manage_stock',
+ 'tax_status',
+ 'cross_sell_ids',
+] satisfies ProductEditFieldId[];
+
+const EXTERNAL_PRODUCT_EDIT_FIELD_IDS = [
+ ...COMMON_PRODUCT_EDIT_FIELD_IDS,
+ 'price',
+ 'regular_price',
+ 'on_sale',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
+ 'external_url',
+ 'button_text',
+ 'tax_status',
+] satisfies ProductEditFieldId[];
+
+const GROUPED_PRODUCT_EDIT_FIELD_IDS = [
+ ...COMMON_PRODUCT_EDIT_FIELD_IDS,
+] as const;
+
+const PRODUCT_TYPE_COMPATIBLE_FIELD_IDS = {
+ simple: SIMPLE_PRODUCT_EDIT_FIELD_IDS,
+ variable: VARIABLE_PRODUCT_EDIT_FIELD_IDS,
+ grouped: GROUPED_PRODUCT_EDIT_FIELD_IDS,
+ external: EXTERNAL_PRODUCT_EDIT_FIELD_IDS,
+} satisfies Record< string, readonly ProductEditFieldId[] >;
function normalizeValue( value: unknown ) {
if ( value === undefined ) {
@@ -59,6 +174,45 @@ function getFieldValue( field: ProductField, item: ProductEntityRecord ) {
return item[ field.id as keyof ProductEntityRecord ];
}
+function getProductTypeCompatibleFieldIds( product: ProductEntityRecord ) {
+ const productType = product.type;
+
+ if ( productType && productType in PRODUCT_TYPE_COMPATIBLE_FIELD_IDS ) {
+ return PRODUCT_TYPE_COMPATIBLE_FIELD_IDS[
+ productType as keyof typeof PRODUCT_TYPE_COMPATIBLE_FIELD_IDS
+ ];
+ }
+
+ return COMMON_PRODUCT_EDIT_FIELD_IDS;
+}
+
+function getCommonProductTypeCompatibleFieldIds(
+ products: ProductEntityRecord[]
+) {
+ if ( products.length === 0 ) {
+ return new Set< string >();
+ }
+
+ const [ firstProduct, ...remainingProducts ] = products;
+ const commonFieldIds = new Set(
+ getProductTypeCompatibleFieldIds( firstProduct )
+ );
+
+ remainingProducts.forEach( ( product ) => {
+ const compatibleFieldIds = new Set(
+ getProductTypeCompatibleFieldIds( product )
+ );
+
+ commonFieldIds.forEach( ( fieldId ) => {
+ if ( ! compatibleFieldIds.has( fieldId ) ) {
+ commonFieldIds.delete( fieldId );
+ }
+ } );
+ } );
+
+ return commonFieldIds;
+}
+
export function getProductEditFields( fields: ProductField[] ): ProductField[] {
return fields.filter(
( field ) =>
@@ -127,13 +281,30 @@ export function getMixedProductEditFieldIds(
export function getVisibleProductEditFields(
fields: ProductField[],
- data: ProductEntityRecord
+ products: ProductEntityRecord[]
) {
- return fields.filter( ( field ) => {
- if ( typeof field.isVisible !== 'function' ) {
- return true;
+ const compatibleFieldIds =
+ getCommonProductTypeCompatibleFieldIds( products );
+
+ return fields.reduce< ProductField[] >( ( visibleFields, field ) => {
+ if ( ! compatibleFieldIds.has( field.id ) ) {
+ return visibleFields;
}
- return field.isVisible( data );
- } );
+ const { isVisible } = field;
+
+ if ( typeof isVisible !== 'function' ) {
+ visibleFields.push( field );
+ return visibleFields;
+ }
+
+ if ( products.every( ( product ) => isVisible( product ) ) ) {
+ visibleFields.push( {
+ ...field,
+ isVisible: undefined,
+ } );
+ }
+
+ return visibleFields;
+ }, [] );
}