Commit 47186eba846 for woocommerce
commit 47186eba846577e9e3b68503b62a136b304b7b8b
Author: Luigi Teschio <gigitux@gmail.com>
Date: Mon May 18 09:48:07 2026 +0200
Update product edit field grouping (#65007)
* Update products quick edit fields
* Add changelog entry for products quick edit fields
* Update product edit field groups
* Add changelog for product edit field groups
* remove field label
diff --git a/packages/js/experimental-products-app/changelog/update-product-edit-field-groups b/packages/js/experimental-products-app/changelog/update-product-edit-field-groups
new file mode 100644
index 00000000000..e93cc31a8f5
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/update-product-edit-field-groups
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Group product edit fields by section and align section spacing with the design.
diff --git a/packages/js/experimental-products-app/src/fields/images/field.tsx b/packages/js/experimental-products-app/src/fields/images/field.tsx
index 3be88e163eb..0a623db7cdb 100644
--- a/packages/js/experimental-products-app/src/fields/images/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/images/field.tsx
@@ -149,7 +149,7 @@ export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
/>
);
},
- Edit: ( { data, onChange, field } ) => {
+ Edit: ( { data, onChange } ) => {
const dataImages = useMemo( () => data.images ?? [], [ data.images ] );
const [ images, setImages ] = useState( dataImages );
@@ -233,8 +233,7 @@ export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
}, [ images, handleRemoveImage ] );
return (
- <Fieldset.Root className="woocommerce-fields-control__images-fieldset">
- <Fieldset.Legend>{ field.label }</Fieldset.Legend>
+ <Fieldset.Root>
<DragDropProvider onDragEnd={ handleDragEnd }>
<div className="woocommerce-fields-control__featured-image">
<div className="woocommerce-fields-controls__featured-image-uploaded-images">
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 89270304f62..7debd7b265a 100644
--- a/packages/js/experimental-products-app/src/product-edit/index.tsx
+++ b/packages/js/experimental-products-app/src/product-edit/index.tsx
@@ -96,7 +96,7 @@ function ProductEditForm( {
const form = {
type: 'regular' as const,
labelPosition: 'top' as const,
- fields: getProductTypeFormFields( selectedProducts ),
+ fields: getProductTypeFormFields( selectedProducts, visibleFields ),
};
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 7c518ba9057..1e13ed4e978 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
@@ -493,11 +493,11 @@ describe( 'product edit utils', () => {
'name',
'product_status',
'catalog_visibility',
+ 'external_url',
+ 'button_text',
'regular_price',
'sale_price',
'images',
- 'external_url',
- 'button_text',
'sku',
'categories',
'brands',
@@ -969,90 +969,198 @@ describe( 'product edit utils', () => {
} );
describe( 'getProductTypeFormFields', () => {
- it( 'uses simple product form config with height last', () => {
+ const getFormFields = ( products: ProductEntityRecord[] ) =>
+ getProductTypeFormFields(
+ products,
+ getVisibleProductEditFields(
+ getProductEditFields( productFields ),
+ products
+ )
+ );
+
+ it( 'uses grouped simple product form config', () => {
const product = buildProduct( {
type: 'simple',
virtual: false,
+ downloadable: true,
+ manage_stock: true,
} );
- expect( getProductTypeFormFields( [ product ] ) ).toEqual( [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'regular_price',
- 'sale_price',
- 'images',
- 'downloadable',
- 'sku',
- 'stock',
- 'manage_stock',
- 'stock_quantity',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
+ expect( getFormFields( [ product ] ) ).toEqual( [
{
- id: 'dimensions',
- layout: { type: 'row' },
- children: [ 'weight', 'length', 'width' ],
+ id: 'general-fields',
+ label: 'General',
+ children: [
+ 'name',
+ 'product_status',
+ 'catalog_visibility',
+ ],
+ },
+ {
+ id: 'price-fields',
+ label: 'Price',
+ children: [ 'regular_price', 'sale_price' ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images', 'downloadable' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku', 'manage_stock', 'stock_quantity' ],
+ },
+ {
+ id: 'product-organization-fields',
+ label: 'Product organization',
+ children: [ 'categories', 'brands', 'tags', 'featured' ],
+ },
+ {
+ id: 'shipping-fields',
+ label: 'Shipping',
+ children: [
+ {
+ id: 'dimensions',
+ layout: { type: 'row' },
+ children: [ 'weight', 'length', 'width' ],
+ },
+ 'height',
+ ],
},
- 'height',
] );
} );
- it( 'uses variable parent form config in design order', () => {
+ it( 'uses grouped variable parent form config', () => {
const product = buildProduct( {
type: 'variable',
virtual: false,
} );
- expect( getProductTypeFormFields( [ product ] ) ).toEqual( [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'images',
- 'sku',
- 'manage_stock',
- 'stock',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
- 'shipping_class',
+ expect( getFormFields( [ product ] ) ).toEqual( [
{
- id: 'parent-dimensions',
- layout: { type: 'row' },
- children: [ 'length', 'width', 'height' ],
+ id: 'general-fields',
+ label: 'General',
+ children: [
+ 'name',
+ 'product_status',
+ 'catalog_visibility',
+ ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku', 'manage_stock', 'stock' ],
+ },
+ {
+ id: 'product-organization-fields',
+ label: 'Product organization',
+ children: [ 'categories', 'brands', 'tags', 'featured' ],
+ },
+ {
+ id: 'shipping-fields',
+ label: 'Shipping',
+ children: [
+ 'shipping_class',
+ {
+ id: 'parent-dimensions',
+ layout: { type: 'row' },
+ children: [ 'length', 'width', 'height' ],
+ },
+ 'weight',
+ ],
},
- 'weight',
] );
} );
- it( 'uses variation product form config', () => {
+ it( 'uses grouped variation product form config', () => {
const product = buildProduct( {
id: 34,
parent_id: 12,
type: 'variation',
virtual: false,
downloadable: true,
+ manage_stock: true,
} );
- expect( getProductTypeFormFields( [ product ] ) ).toEqual( [
- 'product_status',
- 'regular_price',
- 'sale_price',
- 'images',
- 'sku',
- 'manage_stock',
- 'stock',
- 'stock_quantity',
- 'shipping_class',
+ expect( getFormFields( [ product ] ) ).toEqual( [
{
- id: 'dimensions',
- layout: { type: 'row' },
- children: [ 'weight', 'length', 'width' ],
+ id: 'general-fields',
+ label: 'General',
+ children: [ 'product_status' ],
+ },
+ {
+ id: 'price-fields',
+ label: 'Price',
+ children: [ 'regular_price', 'sale_price' ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku', 'manage_stock', 'stock_quantity' ],
+ },
+ {
+ id: 'shipping-fields',
+ label: 'Shipping',
+ children: [
+ 'shipping_class',
+ {
+ id: 'dimensions',
+ layout: { type: 'row' },
+ children: [ 'weight', 'length', 'width' ],
+ },
+ 'height',
+ ],
+ },
+ ] );
+ } );
+
+ it( 'prunes empty groups when all descendants are hidden', () => {
+ const product = buildProduct( {
+ type: 'simple',
+ virtual: true,
+ } );
+
+ expect( getFormFields( [ product ] ) ).toEqual( [
+ {
+ id: 'general-fields',
+ label: 'General',
+ children: [
+ 'name',
+ 'product_status',
+ 'catalog_visibility',
+ ],
+ },
+ {
+ id: 'price-fields',
+ label: 'Price',
+ children: [ 'regular_price', 'sale_price' ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku', 'stock', 'manage_stock' ],
+ },
+ {
+ id: 'product-organization-fields',
+ label: 'Product organization',
+ children: [ 'categories', 'brands', 'tags', 'featured' ],
},
- '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 9958c1c4ea9..d73249ec8cc 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -2,6 +2,7 @@
* External dependencies
*/
import type { Field, FormField } from '@wordpress/dataviews';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -89,84 +90,150 @@ const PARENT_DIMENSIONS_FORM_FIELD: ProductEditFormField = {
children: [ 'length', 'width', 'height' ],
};
+function createProductEditFormGroup(
+ id: string,
+ label: string,
+ children: ProductEditFormField[]
+): ProductEditFormField {
+ return {
+ id,
+ label,
+ children,
+ };
+}
+
const SIMPLE_PRODUCT_EDIT_FORM_FIELDS = [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'regular_price',
- 'sale_price',
- 'images',
- 'downloadable',
- 'sku',
- 'stock',
- 'manage_stock',
- 'stock_quantity',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
- DIMENSIONS_FORM_FIELD,
- 'height',
+ createProductEditFormGroup(
+ 'general-fields',
+ __( 'General', 'woocommerce' ),
+ [ 'name', 'product_status', 'catalog_visibility' ]
+ ),
+ createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
+ 'regular_price',
+ 'sale_price',
+ ] ),
+ createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
+ 'images',
+ 'downloadable',
+ ] ),
+ createProductEditFormGroup(
+ 'inventory-fields',
+ __( 'Inventory', 'woocommerce' ),
+ [ 'sku', 'stock', 'manage_stock', 'stock_quantity' ]
+ ),
+ createProductEditFormGroup(
+ 'product-organization-fields',
+ __( 'Product organization', 'woocommerce' ),
+ [ 'categories', 'brands', 'tags', 'featured' ]
+ ),
+ createProductEditFormGroup(
+ 'shipping-fields',
+ __( 'Shipping', 'woocommerce' ),
+ [ DIMENSIONS_FORM_FIELD, 'height' ]
+ ),
] satisfies ProductEditFormField[];
const VARIATION_PRODUCT_EDIT_FORM_FIELDS = [
- 'product_status',
- 'regular_price',
- 'sale_price',
- 'images',
- 'sku',
- 'manage_stock',
- 'stock',
- 'stock_quantity',
- 'shipping_class',
- DIMENSIONS_FORM_FIELD,
- 'height',
+ createProductEditFormGroup(
+ 'general-fields',
+ __( 'General', 'woocommerce' ),
+ [ 'product_status' ]
+ ),
+ createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
+ 'regular_price',
+ 'sale_price',
+ ] ),
+ createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
+ 'images',
+ ] ),
+ createProductEditFormGroup(
+ 'inventory-fields',
+ __( 'Inventory', 'woocommerce' ),
+ [ 'sku', 'manage_stock', 'stock', 'stock_quantity' ]
+ ),
+ createProductEditFormGroup(
+ 'shipping-fields',
+ __( 'Shipping', 'woocommerce' ),
+ [ 'shipping_class', DIMENSIONS_FORM_FIELD, 'height' ]
+ ),
] satisfies ProductEditFormField[];
const VARIABLE_PRODUCT_EDIT_FORM_FIELDS = [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'images',
- 'sku',
- 'manage_stock',
- 'stock',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
- 'shipping_class',
- PARENT_DIMENSIONS_FORM_FIELD,
- 'weight',
+ createProductEditFormGroup(
+ 'general-fields',
+ __( 'General', 'woocommerce' ),
+ [ 'name', 'product_status', 'catalog_visibility' ]
+ ),
+ createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
+ 'images',
+ ] ),
+ createProductEditFormGroup(
+ 'inventory-fields',
+ __( 'Inventory', 'woocommerce' ),
+ [ 'sku', 'manage_stock', 'stock' ]
+ ),
+ createProductEditFormGroup(
+ 'product-organization-fields',
+ __( 'Product organization', 'woocommerce' ),
+ [ 'categories', 'brands', 'tags', 'featured' ]
+ ),
+ createProductEditFormGroup(
+ 'shipping-fields',
+ __( 'Shipping', 'woocommerce' ),
+ [ 'shipping_class', PARENT_DIMENSIONS_FORM_FIELD, 'weight' ]
+ ),
] satisfies ProductEditFormField[];
const EXTERNAL_PRODUCT_EDIT_FORM_FIELDS = [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'regular_price',
- 'sale_price',
- 'images',
- 'external_url',
- 'button_text',
- 'sku',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
+ createProductEditFormGroup(
+ 'general-fields',
+ __( 'General', 'woocommerce' ),
+ [
+ 'name',
+ 'product_status',
+ 'catalog_visibility',
+ 'external_url',
+ 'button_text',
+ ]
+ ),
+ createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
+ 'regular_price',
+ 'sale_price',
+ ] ),
+ createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
+ 'images',
+ ] ),
+ createProductEditFormGroup(
+ 'inventory-fields',
+ __( 'Inventory', 'woocommerce' ),
+ [ 'sku' ]
+ ),
+ createProductEditFormGroup(
+ 'product-organization-fields',
+ __( 'Product organization', 'woocommerce' ),
+ [ 'categories', 'brands', 'tags', 'featured' ]
+ ),
] satisfies ProductEditFormField[];
const GROUPED_PRODUCT_EDIT_FORM_FIELDS = [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'upsell_ids',
- 'images',
- 'sku',
- 'categories',
- 'brands',
- 'tags',
- 'featured',
+ createProductEditFormGroup(
+ 'general-fields',
+ __( 'General', 'woocommerce' ),
+ [ 'name', 'product_status', 'catalog_visibility', 'upsell_ids' ]
+ ),
+ createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
+ 'images',
+ ] ),
+ createProductEditFormGroup(
+ 'inventory-fields',
+ __( 'Inventory', 'woocommerce' ),
+ [ 'sku' ]
+ ),
+ createProductEditFormGroup(
+ 'product-organization-fields',
+ __( 'Product organization', 'woocommerce' ),
+ [ 'categories', 'brands', 'tags', 'featured' ]
+ ),
] satisfies ProductEditFormField[];
const PRODUCT_TYPE_FORM_FIELDS = {
@@ -250,6 +317,18 @@ export function isProductVariation(
return product.type === 'variation' || Boolean( product.parent_id );
}
+function getProductEditFormFieldIds(
+ formField: ProductEditFormField
+): ProductEditFieldId[] {
+ if ( typeof formField === 'string' ) {
+ return [ formField ];
+ }
+
+ return ( formField.children ?? [] ).flatMap( ( child ) =>
+ getProductEditFormFieldIds( child as ProductEditFormField )
+ );
+}
+
function getProductType( product: ProductEntityRecord ): ProductType {
if ( isProductVariation( product ) ) {
return 'variation';
@@ -262,16 +341,7 @@ function getProductTypeFieldIds(
product: ProductEntityRecord
): 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'
- );
- }
+ getProductEditFormFieldIds
);
}
@@ -500,8 +570,38 @@ export function getVisibleProductEditFields(
);
}
+function pruneProductEditFormField(
+ formField: ProductEditFormField,
+ visibleFieldIds: Set< string >
+): ProductEditFormField | undefined {
+ if ( typeof formField === 'string' ) {
+ return visibleFieldIds.has( formField ) ? formField : undefined;
+ }
+
+ const children = ( formField.children ?? [] )
+ .map( ( child ) =>
+ pruneProductEditFormField(
+ child as ProductEditFormField,
+ visibleFieldIds
+ )
+ )
+ .filter(
+ ( child ): child is ProductEditFormField => child !== undefined
+ );
+
+ if ( children.length === 0 ) {
+ return undefined;
+ }
+
+ return {
+ ...formField,
+ children,
+ };
+}
+
export function getProductTypeFormFields(
- products: ProductEntityRecord[]
+ products: ProductEntityRecord[],
+ visibleFields?: ProductField[]
): Array< FormField | string > {
const [ firstProduct ] = products;
@@ -509,5 +609,24 @@ export function getProductTypeFormFields(
return [];
}
- return [ ...PRODUCT_TYPE_FORM_FIELDS[ getProductType( firstProduct ) ] ];
+ const formFields = [
+ ...PRODUCT_TYPE_FORM_FIELDS[ getProductType( firstProduct ) ],
+ ];
+
+ if ( ! visibleFields ) {
+ return formFields;
+ }
+
+ const visibleFieldIds = new Set(
+ visibleFields.map( ( field ) => field.id )
+ );
+
+ return formFields
+ .map( ( formField ) =>
+ pruneProductEditFormField( formField, visibleFieldIds )
+ )
+ .filter(
+ ( formField ): formField is ProductEditFormField =>
+ formField !== undefined
+ );
}
diff --git a/packages/js/experimental-products-app/src/style.scss b/packages/js/experimental-products-app/src/style.scss
index 694e3cea8ec..8c9336fbece 100644
--- a/packages/js/experimental-products-app/src/style.scss
+++ b/packages/js/experimental-products-app/src/style.scss
@@ -145,6 +145,25 @@
.woocommerce-product-edit__form {
flex: 1;
+
+ // TODO: Improve grouped DataForm section spacing at the DataViews library level.
+ > .dataforms-layouts__wrapper {
+ gap: 0 !important;
+ }
+
+ .dataforms-layouts-regular__header :is(h1, h2, h3, h4, h5, h6) {
+ margin: 0 !important;
+ font-size: 15px !important;
+ }
+
+ .dataforms-layouts-regular__header + .dataforms-layouts__wrapper {
+ gap: 16px !important;
+ margin-top: 16px;
+ }
+
+ .dataforms-layouts__wrapper + .dataforms-layouts-regular__header {
+ margin-top: 32px;
+ }
}
.woocommerce-product-edit__footer {