Commit b1dab68be7d for woocommerce

commit b1dab68be7d0eea25bc0f518312e4f40e9ec9411
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Fri May 8 15:41:06 2026 +0200

    Fix bulk edit fields for variations (#64690)

    * Add embedded variation links to products API

    * Add changelog for variation embeds

    * improve code

    * Align variation embeds with Core behavior

    * Display product variations in products list

    * Add changelog for product list variations

    * lint code

    * Lint code

    Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

    * lint code

    * update changelog

    * lint code

    * fix readme

    * add comment

    * Update products app DataViews hierarchy support

    * update changelog

    * improve code

    * improve logic

    * Fix variation edits in products app

    * Add changelog for variation edit fix

    * Fix bulk edit fields for variations

    * restore pnpm-lock

    ---------

    Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

diff --git a/packages/js/experimental-products-app/changelog/fix-bulk-edit-variations b/packages/js/experimental-products-app/changelog/fix-bulk-edit-variations
new file mode 100644
index 00000000000..f6ec04187b7
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/fix-bulk-edit-variations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show compatible bulk edit fields when editing product variations
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 dbc66826061..70d2a043d44 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
@@ -220,6 +220,42 @@ describe( 'product edit utils', () => {
 				expect( fieldIds ).not.toContain( fieldId );
 			} );
 		};
+		const parentOwnedFieldIds = [
+			'name',
+			'short_description',
+			'description',
+			'product_status',
+			'catalog_visibility',
+			'categories',
+			'tags',
+			'type',
+			'featured',
+			'upsell_ids',
+			'cross_sell_ids',
+			'external_url',
+			'button_text',
+		];
+		const priceFieldIds = [
+			'price',
+			'regular_price',
+			'on_sale',
+			'sale_price',
+			'schedule_sale',
+			'date_on_sale_from',
+			'date_on_sale_to',
+		];
+		const universalFieldIds = [
+			'images',
+			'sku',
+			'manage_stock',
+			'stock_quantity',
+			'weight',
+			'length',
+			'width',
+			'height',
+			'shipping_class',
+			'tax_status',
+		];

 		it( 'shows pricing, shipping, and linked product fields for simple physical products', () => {
 			const fieldIds = getVisibleFieldIds( [
@@ -338,10 +374,11 @@ describe( 'product edit utils', () => {
 			] );
 		} );

-		it( 'hides parent pricing, downloads, and shipping fields for variable products', () => {
+		it( 'hides parent pricing and downloads for variable products', () => {
 			const fieldIds = getVisibleFieldIds( [
 				buildProduct( {
 					type: 'variable',
+					manage_stock: true,
 				} ),
 			] );

@@ -354,44 +391,168 @@ describe( 'product edit utils', () => {
 				'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' ] )
+				expect.arrayContaining( [
+					'upsell_ids',
+					'cross_sell_ids',
+					...universalFieldIds,
+				] )
 			);
 		} );

-		it( 'hides a field in bulk edit if any selected product does not support it', () => {
+		it( 'shows parent-owned and universal fields for simple and variable products', () => {
 			const fieldIds = getVisibleFieldIds( [
 				buildProduct( {
 					id: 1,
 					type: 'simple',
 					virtual: false,
 					downloadable: false,
+					manage_stock: true,
 				} ),
 				buildProduct( {
 					id: 2,
 					type: 'variable',
+					manage_stock: true,
 				} ),
 			] );

+			expectFieldsHidden( fieldIds, priceFieldIds );
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( [
+					'name',
+					'product_status',
+					'catalog_visibility',
+					'categories',
+					'tags',
+					'featured',
+					'upsell_ids',
+					'cross_sell_ids',
+					...universalFieldIds,
+				] )
+			);
+		} );
+
+		it( 'shows sellable instance fields for variations', () => {
+			const fieldIds = getVisibleFieldIds( [
+				buildProduct( {
+					id: 34,
+					parent_id: 12,
+					type: 'variation',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
+			] );
+
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( [
+					...priceFieldIds,
+					...universalFieldIds,
+				] )
+			);
 			expectFieldsHidden( fieldIds, [
-				'price',
-				'regular_price',
-				'on_sale',
-				'weight',
-				'length',
-				'width',
-				'height',
-				'shipping_class',
+				...parentOwnedFieldIds,
+				'downloadable',
+			] );
+		} );
+
+		it( 'shows shared sellable instance fields for simple products and variations', () => {
+			const fieldIds = getVisibleFieldIds( [
+				buildProduct( {
+					id: 1,
+					type: 'simple',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
+				buildProduct( {
+					id: 34,
+					parent_id: 12,
+					type: 'variation',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
 			] );
+
 			expect( fieldIds ).toEqual(
-				expect.arrayContaining( [ 'upsell_ids', 'cross_sell_ids' ] )
+				expect.arrayContaining( [
+					...priceFieldIds,
+					...universalFieldIds,
+				] )
 			);
+			expectFieldsHidden( fieldIds, [
+				...parentOwnedFieldIds,
+				'downloadable',
+			] );
+		} );
+
+		it( 'shows only universal fields for variable products and variations', () => {
+			const fieldIds = getVisibleFieldIds( [
+				buildProduct( {
+					id: 12,
+					type: 'variable',
+					manage_stock: true,
+				} ),
+				buildProduct( {
+					id: 34,
+					parent_id: 12,
+					type: 'variation',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
+			] );
+
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( universalFieldIds )
+			);
+			expectFieldsHidden( fieldIds, [
+				...parentOwnedFieldIds,
+				...priceFieldIds,
+				'downloadable',
+			] );
+		} );
+
+		it( 'shows only universal fields for simple, variable, and variation selections', () => {
+			const fieldIds = getVisibleFieldIds( [
+				buildProduct( {
+					id: 1,
+					type: 'simple',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
+				buildProduct( {
+					id: 12,
+					type: 'variable',
+					manage_stock: true,
+				} ),
+				buildProduct( {
+					id: 34,
+					parent_id: 12,
+					type: 'variation',
+					manage_stock: true,
+					on_sale: true,
+					sale_price: '12',
+					date_on_sale_from: '2026-05-06T00:00:00',
+				} ),
+			] );
+
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( universalFieldIds )
+			);
+			expectFieldsHidden( fieldIds, [
+				...parentOwnedFieldIds,
+				...priceFieldIds,
+				'downloadable',
+			] );
 		} );

 		it( 'does not return visibility predicates after checking selected products', () => {
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 dc1a4daffd7..0a9c0656c27 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -113,10 +113,36 @@ const VARIABLE_PRODUCT_EDIT_FIELD_IDS = [
 	'stock',
 	'stock_quantity',
 	'manage_stock',
+	'weight',
+	'length',
+	'width',
+	'height',
+	'shipping_class',
 	'tax_status',
 	'cross_sell_ids',
 ] satisfies ProductEditFieldId[];

+const VARIATION_PRODUCT_EDIT_FIELD_IDS = [
+	'images',
+	'sku',
+	'price',
+	'regular_price',
+	'on_sale',
+	'sale_price',
+	'schedule_sale',
+	'date_on_sale_from',
+	'date_on_sale_to',
+	'stock',
+	'stock_quantity',
+	'manage_stock',
+	'weight',
+	'length',
+	'width',
+	'height',
+	'shipping_class',
+	'tax_status',
+] satisfies ProductEditFieldId[];
+
 const EXTERNAL_PRODUCT_EDIT_FIELD_IDS = [
 	...COMMON_PRODUCT_EDIT_FIELD_IDS,
 	'price',
@@ -138,10 +164,37 @@ const GROUPED_PRODUCT_EDIT_FIELD_IDS = [
 const PRODUCT_TYPE_COMPATIBLE_FIELD_IDS = {
 	simple: SIMPLE_PRODUCT_EDIT_FIELD_IDS,
 	variable: VARIABLE_PRODUCT_EDIT_FIELD_IDS,
+	variation: VARIATION_PRODUCT_EDIT_FIELD_IDS,
 	grouped: GROUPED_PRODUCT_EDIT_FIELD_IDS,
 	external: EXTERNAL_PRODUCT_EDIT_FIELD_IDS,
 } satisfies Record< string, readonly ProductEditFieldId[] >;

+const PARENT_OWNED_PRODUCT_EDIT_FIELD_ID_SET = new Set< ProductEditFieldId >( [
+	'name',
+	'short_description',
+	'description',
+	'product_status',
+	'catalog_visibility',
+	'categories',
+	'tags',
+	'type',
+	'featured',
+	'upsell_ids',
+	'cross_sell_ids',
+	'external_url',
+	'button_text',
+] );
+
+const SELLABLE_PRODUCT_EDIT_FIELD_ID_SET = new Set< ProductEditFieldId >( [
+	'price',
+	'regular_price',
+	'on_sale',
+	'sale_price',
+	'schedule_sale',
+	'date_on_sale_from',
+	'date_on_sale_to',
+] );
+
 function normalizeValue( value: unknown ) {
 	if ( value === undefined ) {
 		return '__undefined__';
@@ -188,12 +241,46 @@ function getProductTypeCompatibleFieldIds( product: ProductEntityRecord ) {
 	return COMMON_PRODUCT_EDIT_FIELD_IDS;
 }

+function isVariableProductParent( product: ProductEntityRecord ) {
+	return product.type === 'variable' && ! product.parent_id;
+}
+
 export function isProductVariation(
 	product: ProductEntityRecord
 ): product is ProductVariationEntityRecord {
 	return product.type === 'variation' || Boolean( product.parent_id );
 }

+function isFieldVisibleForProductRelationships(
+	fieldId: string,
+	products: ProductEntityRecord[]
+) {
+	if ( ! PRODUCT_EDIT_FIELD_IDS.includes( fieldId as ProductEditFieldId ) ) {
+		return true;
+	}
+
+	const productEditFieldId = fieldId as ProductEditFieldId;
+	const hasVariation = products.some( isProductVariation );
+
+	if (
+		hasVariation &&
+		PARENT_OWNED_PRODUCT_EDIT_FIELD_ID_SET.has( productEditFieldId )
+	) {
+		return false;
+	}
+
+	const hasVariableParent = products.some( isVariableProductParent );
+
+	if (
+		SELLABLE_PRODUCT_EDIT_FIELD_ID_SET.has( productEditFieldId ) &&
+		hasVariableParent
+	) {
+		return false;
+	}
+
+	return true;
+}
+
 export function getProductVariationUpdatePath(
 	product: ProductVariationEntityRecord
 ) {
@@ -354,6 +441,10 @@ export function getVisibleProductEditFields(
 			return visibleFields;
 		}

+		if ( ! isFieldVisibleForProductRelationships( field.id, products ) ) {
+			return visibleFields;
+		}
+
 		const { isVisible } = field;

 		if ( typeof isVisible !== 'function' ) {