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' ) {