Commit 0d246c57aa6 for woocommerce
commit 0d246c57aa69caef62e7834f8a5d8d7ef90a556f
Author: Luigi Teschio <gigitux@gmail.com>
Date: Mon May 18 11:25:18 2026 +0200
Add product edit sale scheduling and shipping fields (#65116)
* Add product sale scheduling fields
* Add changelog entry for sale scheduling fields
* Add shipping class to product edit forms
* Update changelog entry for product edit fields
* Remove grouped product price edit section
* Add variation sale schedule layout coverage
* remove shipping fields for external product
* Move affiliate buy button fields after images
diff --git a/packages/js/experimental-products-app/changelog/update-sale-schedule-fields b/packages/js/experimental-products-app/changelog/update-sale-schedule-fields
new file mode 100644
index 00000000000..f2a39ca8f56
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/update-sale-schedule-fields
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add sale scheduling and shipping class fields to product edit forms
diff --git a/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx b/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx
index b657caad17e..11829d57a0e 100644
--- a/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx
@@ -33,9 +33,6 @@ const fieldDefinition = {
export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
...fieldDefinition,
- isVisible: ( item ) => {
- return !! item.on_sale || !! item.sale_price;
- },
Edit: ( { data, onChange, field } ) => {
const toggleId = useInstanceId( FormToggle, 'schedule-sale-toggle' );
const [ tempDateOnSaleFrom, setTempDateOnSaleFrom ] = useState(
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 1e13ed4e978..30fa0b192ef 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
@@ -349,6 +349,9 @@ describe( 'product edit utils', () => {
'catalog_visibility',
'regular_price',
'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
'images',
'sku',
'stock',
@@ -357,6 +360,7 @@ describe( 'product edit utils', () => {
'brands',
'tags',
'featured',
+ 'shipping_class',
'weight',
'length',
'width',
@@ -364,13 +368,9 @@ describe( 'product edit utils', () => {
] );
expectFieldsHidden( fieldIds, [
'price',
- 'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'downloadable',
'external_url',
'button_text',
- 'shipping_class',
'tax_status',
'upsell_ids',
'cross_sell_ids',
@@ -444,10 +444,10 @@ describe( 'product edit utils', () => {
'length',
'width',
'height',
+ 'shipping_class',
] )
);
expectFieldOrder( fieldIds, [ 'images', 'downloadable', 'sku' ] );
- expectFieldsHidden( fieldIds, [ 'shipping_class' ] );
} );
it( 'shows grouped product fields in quick edit order', () => {
@@ -470,7 +470,12 @@ describe( 'product edit utils', () => {
'featured',
] );
expectFieldsHidden( fieldIds, [
- ...priceFieldIds,
+ 'price',
+ 'regular_price',
+ 'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
'downloadable',
'cross_sell_ids',
'external_url',
@@ -493,11 +498,12 @@ describe( 'product edit utils', () => {
'name',
'product_status',
'catalog_visibility',
- 'external_url',
- 'button_text',
'regular_price',
'sale_price',
+ 'schedule_sale',
'images',
+ 'external_url',
+ 'button_text',
'sku',
'categories',
'brands',
@@ -506,13 +512,16 @@ describe( 'product edit utils', () => {
] );
expectFieldsHidden( fieldIds, [
'price',
- 'schedule_sale',
'date_on_sale_from',
'date_on_sale_to',
'cross_sell_ids',
'downloadable',
'upsell_ids',
- ...shippingFieldIds,
+ 'weight',
+ 'length',
+ 'width',
+ 'height',
+ 'shipping_class',
...stockStatusFieldIds,
'stock_quantity',
] );
@@ -591,6 +600,7 @@ describe( 'product edit utils', () => {
'featured',
'images',
'manage_stock',
+ 'shipping_class',
'weight',
'length',
'width',
@@ -600,7 +610,6 @@ describe( 'product edit utils', () => {
expectFieldsHidden( fieldIds, [
'upsell_ids',
'cross_sell_ids',
- 'shipping_class',
'tax_status',
'stock_quantity',
] );
@@ -629,10 +638,10 @@ describe( 'product edit utils', () => {
expect.arrayContaining( basePriceFieldIds )
);
expectFieldsHidden( fieldIds, [
- 'schedule_sale',
'date_on_sale_from',
'date_on_sale_to',
] );
+ expect( fieldIds ).toContain( 'schedule_sale' );
expectFieldsHidden( fieldIds, [ 'sku' ] );
} );
@@ -654,6 +663,9 @@ describe( 'product edit utils', () => {
'product_status',
'regular_price',
'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
'images',
'sku',
'manage_stock',
@@ -670,9 +682,6 @@ describe( 'product edit utils', () => {
'stock',
'downloadable',
'price',
- 'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'tax_status',
] );
} );
@@ -695,6 +704,9 @@ describe( 'product edit utils', () => {
'sku',
'regular_price',
'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
'stock',
'manage_stock',
'product_status',
@@ -706,13 +718,7 @@ describe( 'product edit utils', () => {
] )
);
expectFieldsHidden( fieldIds, parentOwnedFieldIds );
- expectFieldsHidden( fieldIds, [
- 'price',
- 'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
- 'tax_status',
- ] );
+ expectFieldsHidden( fieldIds, [ 'price', 'tax_status' ] );
} );
it( 'hides shipping fields for virtual variations', () => {
@@ -776,6 +782,9 @@ describe( 'product edit utils', () => {
expect.arrayContaining( [
'regular_price',
'sale_price',
+ 'schedule_sale',
+ 'date_on_sale_from',
+ 'date_on_sale_to',
...bulkSellableInstanceFieldIds,
] )
);
@@ -784,9 +793,6 @@ describe( 'product edit utils', () => {
'downloadable',
'sku',
'price',
- 'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
] );
} );
@@ -928,6 +934,7 @@ describe( 'product edit utils', () => {
'product_status',
'images',
'manage_stock',
+ 'shipping_class',
'weight',
'length',
'width',
@@ -941,7 +948,6 @@ describe( 'product edit utils', () => {
'sku',
'stock',
'stock_quantity',
- 'shipping_class',
'tax_status',
] );
} );
@@ -999,7 +1005,11 @@ describe( 'product edit utils', () => {
{
id: 'price-fields',
label: 'Price',
- children: [ 'regular_price', 'sale_price' ],
+ children: [
+ 'regular_price',
+ 'sale_price',
+ 'schedule_sale',
+ ],
},
{
id: 'image-fields',
@@ -1020,6 +1030,7 @@ describe( 'product edit utils', () => {
id: 'shipping-fields',
label: 'Shipping',
children: [
+ 'shipping_class',
{
id: 'dimensions',
layout: { type: 'row' },
@@ -1031,6 +1042,124 @@ describe( 'product edit utils', () => {
] );
} );
+ it.each( [
+ [ 'simple product', 'simple' ],
+ [ 'variation product', 'variation' ],
+ ] as const )(
+ 'groups sale schedule date fields on the same row for %s',
+ ( _label, productType ) => {
+ const product = buildProduct( {
+ type: productType,
+ date_on_sale_from: '2026-05-06T00:00:00',
+ } );
+
+ const priceGroup = getFormFields( [ product ] ).find(
+ ( formField ) =>
+ typeof formField !== 'string' &&
+ formField.id === 'price-fields'
+ );
+
+ expect( priceGroup ).toEqual( {
+ id: 'price-fields',
+ label: 'Price',
+ children: [
+ 'regular_price',
+ 'sale_price',
+ 'schedule_sale',
+ {
+ id: 'sale-schedule-dates',
+ layout: { type: 'row' },
+ children: [
+ 'date_on_sale_from',
+ 'date_on_sale_to',
+ ],
+ },
+ ],
+ } );
+ }
+ );
+
+ it( 'omits the price section from grouped product form config', () => {
+ const product = buildProduct( {
+ type: 'grouped',
+ } );
+
+ expect( getFormFields( [ product ] ) ).toEqual( [
+ {
+ id: 'general-fields',
+ label: 'General',
+ children: [
+ 'name',
+ 'product_status',
+ 'catalog_visibility',
+ 'upsell_ids',
+ ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku' ],
+ },
+ {
+ id: 'product-organization-fields',
+ label: 'Product organization',
+ children: [ 'categories', 'brands', 'tags', 'featured' ],
+ },
+ ] );
+ } );
+
+ it( 'uses grouped external product form config', () => {
+ const product = buildProduct( {
+ type: 'external',
+ } );
+
+ 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',
+ 'schedule_sale',
+ ],
+ },
+ {
+ id: 'image-fields',
+ label: 'Images',
+ children: [ 'images' ],
+ },
+ {
+ id: 'buy-button-fields',
+ label: 'Buy button',
+ children: [ 'external_url', 'button_text' ],
+ },
+ {
+ id: 'inventory-fields',
+ label: 'Inventory',
+ children: [ 'sku' ],
+ },
+ {
+ id: 'product-organization-fields',
+ label: 'Product organization',
+ children: [ 'categories', 'brands', 'tags', 'featured' ],
+ },
+ ] );
+ } );
+
it( 'uses grouped variable parent form config', () => {
const product = buildProduct( {
type: 'variable',
@@ -1097,7 +1226,11 @@ describe( 'product edit utils', () => {
{
id: 'price-fields',
label: 'Price',
- children: [ 'regular_price', 'sale_price' ],
+ children: [
+ 'regular_price',
+ 'sale_price',
+ 'schedule_sale',
+ ],
},
{
id: 'image-fields',
@@ -1144,7 +1277,11 @@ describe( 'product edit utils', () => {
{
id: 'price-fields',
label: 'Price',
- children: [ 'regular_price', 'sale_price' ],
+ children: [
+ 'regular_price',
+ 'sale_price',
+ 'schedule_sale',
+ ],
},
{
id: 'image-fields',
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 d73249ec8cc..e31f7005d2f 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -111,6 +111,12 @@ const SIMPLE_PRODUCT_EDIT_FORM_FIELDS = [
createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
'regular_price',
'sale_price',
+ 'schedule_sale',
+ {
+ id: 'sale-schedule-dates',
+ layout: { type: 'row' as const },
+ children: [ 'date_on_sale_from', 'date_on_sale_to' ],
+ },
] ),
createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
'images',
@@ -129,7 +135,7 @@ const SIMPLE_PRODUCT_EDIT_FORM_FIELDS = [
createProductEditFormGroup(
'shipping-fields',
__( 'Shipping', 'woocommerce' ),
- [ DIMENSIONS_FORM_FIELD, 'height' ]
+ [ 'shipping_class', DIMENSIONS_FORM_FIELD, 'height' ]
),
] satisfies ProductEditFormField[];
@@ -142,6 +148,12 @@ const VARIATION_PRODUCT_EDIT_FORM_FIELDS = [
createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
'regular_price',
'sale_price',
+ 'schedule_sale',
+ {
+ id: 'sale-schedule-dates',
+ layout: { type: 'row' as const },
+ children: [ 'date_on_sale_from', 'date_on_sale_to' ],
+ },
] ),
createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
'images',
@@ -188,21 +200,26 @@ const EXTERNAL_PRODUCT_EDIT_FORM_FIELDS = [
createProductEditFormGroup(
'general-fields',
__( 'General', 'woocommerce' ),
- [
- 'name',
- 'product_status',
- 'catalog_visibility',
- 'external_url',
- 'button_text',
- ]
+ [ 'name', 'product_status', 'catalog_visibility' ]
),
createProductEditFormGroup( 'price-fields', __( 'Price', 'woocommerce' ), [
'regular_price',
'sale_price',
+ 'schedule_sale',
+ {
+ id: 'sale-schedule-dates',
+ layout: { type: 'row' as const },
+ children: [ 'date_on_sale_from', 'date_on_sale_to' ],
+ },
] ),
createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
'images',
] ),
+ createProductEditFormGroup(
+ 'buy-button-fields',
+ __( 'Buy button', 'woocommerce' ),
+ [ 'external_url', 'button_text' ]
+ ),
createProductEditFormGroup(
'inventory-fields',
__( 'Inventory', 'woocommerce' ),