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 {