Commit 24c9f405c95 for woocommerce

commit 24c9f405c95d195bb105668d566c5e0e31fcb7ef
Author: verofasulo <98944206+verofasulo@users.noreply.github.com>
Date:   Thu May 14 12:33:32 2026 +0200

    Render dimension fields side-by-side in the quick edit panel (#64785)

    * Render dimension fields side-by-side in the quick edit panel

    Group the weight, length, width, and height fields into a single
    DataForm row layout so they render on one line instead of stacking
    vertically. Adds buildProductEditFormFields() which transforms a
    flat list of visible field ids into the DataForm fields config,
    wrapping the dimension subset in a row-layout FormField. Wired
    into ProductEditForm in product-edit/index.tsx.

    * Simplify product edit dimension form layout

    ---------

    Co-authored-by: Luigi Teschio <gigitux@gmail.com>

diff --git a/packages/js/experimental-products-app/changelog/update-dimensions-row-layout b/packages/js/experimental-products-app/changelog/update-dimensions-row-layout
new file mode 100644
index 00000000000..616c5f4a810
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/update-dimensions-row-layout
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Render the weight, length, and width fields side-by-side with height last in the experimental products quick edit panel.
diff --git a/packages/js/experimental-products-app/src/fields/components/dimension.tsx b/packages/js/experimental-products-app/src/fields/components/dimension.tsx
index 1a42f1016c1..42fce7e14ff 100644
--- a/packages/js/experimental-products-app/src/fields/components/dimension.tsx
+++ b/packages/js/experimental-products-app/src/fields/components/dimension.tsx
@@ -13,11 +13,18 @@ import type { ProductEntityRecord, SettingsEntityRecord } from '../types';

 export type DimensionKey = 'height' | 'width' | 'length';

+export function isDimensionVisible( item: ProductEntityRecord ) {
+	return (
+		! item.virtual &&
+		( ( item.type === 'simple' && ! item.parent_id ) || item.downloadable )
+	);
+}
+
 export const createDimensionField = (
 	key: DimensionKey
 ): Partial< Field< ProductEntityRecord > > => {
 	return {
-		isVisible: ( item ) => ! item.virtual,
+		isVisible: isDimensionVisible,
 		Edit: ( { data, onChange, field } ) => {
 			const {
 				record: storeProductsSettings,
diff --git a/packages/js/experimental-products-app/src/fields/weight/field.tsx b/packages/js/experimental-products-app/src/fields/weight/field.tsx
index f4473271a15..5343de54179 100644
--- a/packages/js/experimental-products-app/src/fields/weight/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/weight/field.tsx
@@ -11,6 +11,7 @@ import type { Field } from '@wordpress/dataviews';
  */

 import type { ProductEntityRecord, SettingsEntityRecord } from '../types';
+import { isDimensionVisible } from '../components/dimension';

 const fieldDefinition = {
 	type: 'text',
@@ -23,7 +24,7 @@ const fieldDefinition = {
 export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
 	...fieldDefinition,
 	label: __( 'Weight', 'woocommerce' ),
-	isVisible: ( item ) => ! item.virtual,
+	isVisible: isDimensionVisible,
 	Edit: ( { data, onChange, field } ) => {
 		const {
 			record: storeProductsSettings,
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 1af74d40718..89270304f62 100644
--- a/packages/js/experimental-products-app/src/product-edit/index.tsx
+++ b/packages/js/experimental-products-app/src/product-edit/index.tsx
@@ -27,6 +27,7 @@ import {
 	getProductEditRecord,
 	getProductWithUpdatedVariation,
 	getProductEditFields,
+	getProductTypeFormFields,
 	getVisibleProductEditFields,
 	isProductVariation,
 } from './utils';
@@ -95,7 +96,7 @@ function ProductEditForm( {
 	const form = {
 		type: 'regular' as const,
 		labelPosition: 'top' as const,
-		fields: visibleFields.map( ( field ) => field.id ),
+		fields: getProductTypeFormFields( selectedProducts ),
 	};

 	return (
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 e1c7c3647b3..c9656c5b737 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
@@ -15,6 +15,7 @@ import {
 	getProductEditFields,
 	getProductEditRecord,
 	getProductVariationUpdatePath,
+	getProductTypeFormFields,
 	getVisibleProductEditFields,
 	isProductVariation,
 } from './utils';
@@ -358,6 +359,10 @@ describe( 'product edit utils', () => {
 				'categories',
 				'brands',
 				'tags',
+				'weight',
+				'length',
+				'width',
+				'height',
 			] );
 			expectFieldsHidden( fieldIds, [
 				'price',
@@ -367,7 +372,7 @@ describe( 'product edit utils', () => {
 				'downloadable',
 				'external_url',
 				'button_text',
-				...shippingFieldIds,
+				'shipping_class',
 				'tax_status',
 				'upsell_ids',
 				'cross_sell_ids',
@@ -440,8 +445,16 @@ describe( 'product edit utils', () => {
 			] );

 			expect( fieldIds ).toContain( 'downloadable' );
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( [
+					'weight',
+					'length',
+					'width',
+					'height',
+				] )
+			);
 			expectFieldOrder( fieldIds, [ 'images', 'downloadable', 'sku' ] );
-			expectFieldsHidden( fieldIds, shippingFieldIds );
+			expectFieldsHidden( fieldIds, [ 'shipping_class' ] );
 		} );

 		it( 'shows grouped product fields in quick edit order', () => {
@@ -537,11 +550,17 @@ describe( 'product edit utils', () => {
 					'cross_sell_ids',
 					'stock',
 					'manage_stock',
-					...shippingFieldIds,
+					'shipping_class',
 					'tax_status',
 				] )
 			);
-			expectFieldsHidden( fieldIds, [ 'stock_quantity' ] );
+			expectFieldsHidden( fieldIds, [
+				'stock_quantity',
+				'weight',
+				'length',
+				'width',
+				'height',
+			] );
 		} );

 		it( 'shows parent-owned and universal fields for simple and variable products', () => {
@@ -576,8 +595,12 @@ describe( 'product edit utils', () => {
 				'featured',
 				'upsell_ids',
 				'cross_sell_ids',
-				...shippingFieldIds,
+				'shipping_class',
 				'tax_status',
+				'weight',
+				'length',
+				'width',
+				'height',
 			] );
 		} );

@@ -698,6 +721,28 @@ describe( 'product edit utils', () => {
 			expectFieldsHidden( fieldIds, shippingFieldIds );
 		} );

+		it( 'shows dimensions for physical downloadable variations', () => {
+			const fieldIds = getVisibleFieldIds( [
+				buildProduct( {
+					id: 34,
+					parent_id: 12,
+					type: 'variation',
+					virtual: false,
+					downloadable: true,
+				} ),
+			] );
+
+			expect( fieldIds ).toEqual(
+				expect.arrayContaining( [
+					'downloadable',
+					'weight',
+					'length',
+					'width',
+					'height',
+				] )
+			);
+		} );
+
 		it( 'shows shared sellable instance fields for simple products and variations', () => {
 			const fieldIds = getVisibleFieldIds( [
 				buildProduct( {
@@ -895,4 +940,65 @@ describe( 'product edit utils', () => {
 			expect( field?.isVisible ).toBeUndefined();
 		} );
 	} );
+
+	describe( 'getProductTypeFormFields', () => {
+		it( 'uses simple product form config with height last', () => {
+			const product = buildProduct( {
+				type: 'simple',
+				virtual: false,
+			} );
+
+			expect( getProductTypeFormFields( [ product ] ) ).toEqual( [
+				'name',
+				'product_status',
+				'catalog_visibility',
+				'regular_price',
+				'on_sale',
+				'sale_price',
+				'images',
+				'downloadable',
+				'sku',
+				'stock',
+				'manage_stock',
+				'stock_quantity',
+				'categories',
+				'brands',
+				'tags',
+				{
+					id: 'dimensions',
+					layout: { type: 'row' },
+					children: [ 'weight', 'length', 'width' ],
+				},
+				'height',
+			] );
+		} );
+
+		it( 'uses variation product form config', () => {
+			const product = buildProduct( {
+				id: 34,
+				parent_id: 12,
+				type: 'variation',
+				virtual: false,
+				downloadable: true,
+			} );
+
+			expect( getProductTypeFormFields( [ product ] ) ).toEqual( [
+				'regular_price',
+				'on_sale',
+				'sale_price',
+				'images',
+				'downloadable',
+				'sku',
+				'stock',
+				'manage_stock',
+				'stock_quantity',
+				{
+					id: 'dimensions',
+					layout: { type: 'row' },
+					children: [ 'weight', 'length', 'width' ],
+				},
+				'height',
+			] );
+		} );
+	} );
 } );
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 6f45631691a..77453d63940 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import type { Field } from '@wordpress/dataviews';
+import type { Field, FormField } from '@wordpress/dataviews';

 /**
  * Internal dependencies
@@ -25,6 +25,8 @@ const EXCLUDED_PRODUCT_EDIT_FIELD_ID_SET = new Set(

 type ProductField = Field< ProductEntityRecord >;
 type ProductEditFieldId = ( typeof PRODUCT_EDIT_FIELD_IDS )[ number ];
+type ProductEditFormField = ProductEditFieldId | FormField;
+type ProductType = 'simple' | 'variation' | 'variable' | 'grouped' | 'external';
 type ProductVariationEntityRecord = ProductEntityRecord & {
 	parent_id: number;
 };
@@ -73,7 +75,15 @@ const PRODUCT_EDIT_FIELD_IDS = [
 	'linked_products_count',
 ] as const;

-const SIMPLE_PRODUCT_EDIT_FIELD_IDS = [
+const DIMENSION_GROUP_FIELD_IDS = [ 'weight', 'length', 'width' ] as const;
+
+const DIMENSIONS_FORM_FIELD: ProductEditFormField = {
+	id: 'dimensions',
+	layout: { type: 'row' as const },
+	children: [ ...DIMENSION_GROUP_FIELD_IDS ],
+};
+
+const SIMPLE_PRODUCT_EDIT_FORM_FIELDS = [
 	'name',
 	'product_status',
 	'catalog_visibility',
@@ -89,9 +99,25 @@ const SIMPLE_PRODUCT_EDIT_FIELD_IDS = [
 	'categories',
 	'brands',
 	'tags',
-] satisfies ProductEditFieldId[];
+	DIMENSIONS_FORM_FIELD,
+	'height',
+] satisfies ProductEditFormField[];
+
+const VARIATION_PRODUCT_EDIT_FORM_FIELDS = [
+	'regular_price',
+	'on_sale',
+	'sale_price',
+	'images',
+	'downloadable',
+	'sku',
+	'stock',
+	'manage_stock',
+	'stock_quantity',
+	DIMENSIONS_FORM_FIELD,
+	'height',
+] satisfies ProductEditFormField[];

-const VARIABLE_PRODUCT_EDIT_FIELD_IDS = [
+const VARIABLE_PRODUCT_EDIT_FORM_FIELDS = [
 	'name',
 	'short_description',
 	'description',
@@ -101,10 +127,6 @@ const VARIABLE_PRODUCT_EDIT_FIELD_IDS = [
 	'stock',
 	'stock_quantity',
 	'manage_stock',
-	'weight',
-	'length',
-	'width',
-	'height',
 	'shipping_class',
 	'tax_status',
 	'categories',
@@ -113,9 +135,11 @@ const VARIABLE_PRODUCT_EDIT_FIELD_IDS = [
 	'catalog_visibility',
 	'upsell_ids',
 	'cross_sell_ids',
-] satisfies ProductEditFieldId[];
+	DIMENSIONS_FORM_FIELD,
+	'height',
+] satisfies ProductEditFormField[];

-const EXTERNAL_PRODUCT_EDIT_FIELD_IDS = [
+const EXTERNAL_PRODUCT_EDIT_FORM_FIELDS = [
 	'name',
 	'product_status',
 	'catalog_visibility',
@@ -130,9 +154,9 @@ const EXTERNAL_PRODUCT_EDIT_FIELD_IDS = [
 	'brands',
 	'tags',
 	'featured',
-] satisfies ProductEditFieldId[];
+] satisfies ProductEditFormField[];

-const GROUPED_PRODUCT_EDIT_FIELD_IDS = [
+const GROUPED_PRODUCT_EDIT_FORM_FIELDS = [
 	'name',
 	'product_status',
 	'catalog_visibility',
@@ -143,17 +167,15 @@ const GROUPED_PRODUCT_EDIT_FIELD_IDS = [
 	'brands',
 	'tags',
 	'featured',
-] satisfies ProductEditFieldId[];
-
-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<
-	'simple' | 'variable' | 'grouped' | 'external',
-	readonly ProductEditFieldId[]
->;
+] satisfies ProductEditFormField[];
+
+const PRODUCT_TYPE_FORM_FIELDS = {
+	simple: SIMPLE_PRODUCT_EDIT_FORM_FIELDS,
+	variation: VARIATION_PRODUCT_EDIT_FORM_FIELDS,
+	variable: VARIABLE_PRODUCT_EDIT_FORM_FIELDS,
+	grouped: GROUPED_PRODUCT_EDIT_FORM_FIELDS,
+	external: EXTERNAL_PRODUCT_EDIT_FORM_FIELDS,
+} satisfies Record< ProductType, readonly ProductEditFormField[] >;

 const PARENT_OWNED_PRODUCT_EDIT_FIELD_ID_SET = new Set< ProductEditFieldId >( [
 	'name',
@@ -213,23 +235,45 @@ function isVariableProductParent( product: ProductEntityRecord ) {
 	return product.type === 'variable' && ! product.parent_id;
 }

+function isProductType( type: string | undefined ): type is ProductType {
+	return (
+		type === 'simple' ||
+		type === 'variation' ||
+		type === 'variable' ||
+		type === 'grouped' ||
+		type === 'external'
+	);
+}
+
 export function isProductVariation(
 	product: ProductEntityRecord
 ): product is ProductVariationEntityRecord {
 	return product.type === 'variation' || Boolean( product.parent_id );
 }

-function getProductTypeCompatibleFieldIds(
+function getProductType( product: ProductEntityRecord ): ProductType {
+	if ( isProductVariation( product ) ) {
+		return 'variation';
+	}
+
+	return isProductType( product.type ) ? product.type : 'simple';
+}
+
+function getProductTypeFieldIds(
 	product: ProductEntityRecord
-): readonly ProductEditFieldId[] {
-	const productType =
-		product.type === 'variable' ||
-		product.type === 'grouped' ||
-		product.type === 'external'
-			? product.type
-			: 'simple';
-
-	return PRODUCT_TYPE_COMPATIBLE_FIELD_IDS[ productType ];
+): ProductEditFieldId[] {
+	return PRODUCT_TYPE_FORM_FIELDS[ getProductType( product ) ].flatMap(
+		( formField ) => {
+			if ( typeof formField === 'string' ) {
+				return [ formField ];
+			}
+
+			return ( formField.children ?? [] ).filter(
+				( child ): child is ProductEditFieldId =>
+					typeof child === 'string'
+			);
+		}
+	);
 }

 function isFieldVisibleForProductRelationships(
@@ -352,14 +396,13 @@ function getCommonProductTypeCompatibleFieldIds(

 	const [ firstProduct, ...remainingProducts ] = products;
 	const remainingCompatibleFieldIdSets = remainingProducts.map(
-		( product ) => new Set( getProductTypeCompatibleFieldIds( product ) )
+		( product ) => new Set( getProductTypeFieldIds( product ) )
 	);

-	return getProductTypeCompatibleFieldIds( firstProduct ).filter(
-		( fieldId ) =>
-			remainingCompatibleFieldIdSets.every( ( compatibleFieldIds ) =>
-				compatibleFieldIds.has( fieldId )
-			)
+	return getProductTypeFieldIds( firstProduct ).filter( ( fieldId ) =>
+		remainingCompatibleFieldIdSets.every( ( compatibleFieldIds ) =>
+			compatibleFieldIds.has( fieldId )
+		)
 	);
 }

@@ -457,3 +500,15 @@ export function getVisibleProductEditFields(
 		[]
 	);
 }
+
+export function getProductTypeFormFields(
+	products: ProductEntityRecord[]
+): Array< FormField | string > {
+	const [ firstProduct ] = products;
+
+	if ( ! firstProduct ) {
+		return [];
+	}
+
+	return [ ...PRODUCT_TYPE_FORM_FIELDS[ getProductType( firstProduct ) ] ];
+}