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