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;
+	}, [] );
 }