Commit 23a4a918769 for woocommerce
commit 23a4a918769d9e512f336a1f19823e1c829c7d29
Author: Luigi Teschio <gigitux@gmail.com>
Date: Wed May 20 16:16:01 2026 +0200
Update Products App schedule sale control (#65195)
diff --git a/packages/js/experimental-products-app/changelog/update-schedule-sale-control b/packages/js/experimental-products-app/changelog/update-schedule-sale-control
new file mode 100644
index 00000000000..f1472aac8f8
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/update-schedule-sale-control
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Move the sale schedule date pickers into the schedule sale field.
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 11829d57a0e..8cb68928296 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
@@ -1,16 +1,9 @@
/**
* External dependencies
*/
-import {
- BaseControl,
- FlexBlock,
- FormToggle,
- __experimentalHStack as HStack,
-} from '@wordpress/components';
-
-import { useInstanceId } from '@wordpress/compose';
+import { CheckboxControl } from '@wordpress/components';
-import { useState } from '@wordpress/element';
+import { useCallback, useMemo, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
@@ -22,6 +15,11 @@ import type { Field } from '@wordpress/dataviews';
import type { ProductEntityRecord } from '../types';
import { getLocalDefaultSaleStart } from '../price/utils';
+import {
+ DatePicker,
+ formatDateTimeLocal,
+ parseDateTimeLocal,
+} from '../components/date-picker';
const fieldDefinition = {
type: 'boolean',
@@ -34,7 +32,6 @@ const fieldDefinition = {
export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
...fieldDefinition,
Edit: ( { data, onChange, field } ) => {
- const toggleId = useInstanceId( FormToggle, 'schedule-sale-toggle' );
const [ tempDateOnSaleFrom, setTempDateOnSaleFrom ] = useState(
data.date_on_sale_from || ''
);
@@ -42,51 +39,152 @@ export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
data.date_on_sale_to || ''
);
const checked = !! data.date_on_sale_to || !! data.date_on_sale_from;
+ const today = useMemo( () => {
+ const d = new Date();
+ d.setHours( 0, 0, 0, 0 );
+ return d;
+ }, [] );
+ const dateOnSaleFrom = useMemo(
+ () =>
+ typeof data.date_on_sale_from === 'string' &&
+ data.date_on_sale_from
+ ? parseDateTimeLocal( data.date_on_sale_from )
+ : null,
+ [ data.date_on_sale_from ]
+ );
+ const minDateOnSaleTo = useMemo( () => {
+ if ( dateOnSaleFrom ) {
+ const min = new Date( dateOnSaleFrom );
+ min.setMinutes( min.getMinutes() + 1 );
+ return min;
+ }
+
+ return today;
+ }, [ dateOnSaleFrom, today ] );
+ const handleScheduleChange = useCallback(
+ ( value: boolean ) => {
+ if ( ! value ) {
+ setTempDateOnSaleFrom( data.date_on_sale_from || '' );
+ setTempDateOnSaleTo( data.date_on_sale_to || '' );
+ onChange( {
+ date_on_sale_from: '',
+ date_on_sale_to: '',
+ } );
+ return;
+ }
+
+ let nextDateOnSaleFrom =
+ data.date_on_sale_from || tempDateOnSaleFrom;
+ const nextDateOnSaleTo =
+ data.date_on_sale_to || tempDateOnSaleTo;
+
+ if ( ! nextDateOnSaleFrom && ! nextDateOnSaleTo ) {
+ nextDateOnSaleFrom = getLocalDefaultSaleStart();
+ }
+
+ onChange( {
+ date_on_sale_from: nextDateOnSaleFrom,
+ date_on_sale_to: nextDateOnSaleTo,
+ } );
+ },
+ [
+ data.date_on_sale_from,
+ data.date_on_sale_to,
+ onChange,
+ tempDateOnSaleFrom,
+ tempDateOnSaleTo,
+ ]
+ );
+ const handleDateOnSaleFromChange = useCallback(
+ ( value: { date_on_sale_from?: string | null } ) => {
+ const newStart = value.date_on_sale_from;
+ const currentEnd = data.date_on_sale_to;
+
+ if (
+ typeof newStart !== 'string' ||
+ ! newStart ||
+ typeof currentEnd !== 'string' ||
+ ! currentEnd
+ ) {
+ onChange( value );
+ return;
+ }
+
+ const startDate = parseDateTimeLocal( newStart );
+ const endDate = parseDateTimeLocal( currentEnd );
+
+ if (
+ startDate &&
+ endDate &&
+ startDate.getTime() >= endDate.getTime()
+ ) {
+ const newEndDate = new Date( startDate );
+ newEndDate.setDate( newEndDate.getDate() + 1 );
+
+ onChange( {
+ ...value,
+ date_on_sale_to: formatDateTimeLocal( newEndDate ),
+ } );
+ return;
+ }
+
+ onChange( value );
+ },
+ [ data.date_on_sale_to, onChange ]
+ );
+
return (
- <BaseControl className="components-toggle-control">
- <HStack justify="flex-start" spacing={ 2 }>
- <FormToggle
- id={ toggleId }
- checked={ checked }
- onChange={ () => {
- if ( checked ) {
- setTempDateOnSaleFrom(
- data.date_on_sale_from || ''
- );
- setTempDateOnSaleTo(
- data.date_on_sale_to || ''
- );
- onChange( {
- date_on_sale_from: '',
- date_on_sale_to: '',
- } );
- } else {
- let dateOnSaleFrom =
- data.date_on_sale_from ||
- tempDateOnSaleFrom;
- const dateOnSaleTo =
- data.date_on_sale_to || tempDateOnSaleTo;
-
- if ( ! dateOnSaleFrom && ! dateOnSaleTo ) {
- dateOnSaleFrom = getLocalDefaultSaleStart();
- }
-
- onChange( {
- date_on_sale_from: dateOnSaleFrom,
- date_on_sale_to: dateOnSaleTo,
- } );
- }
- } }
- />
- <FlexBlock
- as="label"
- htmlFor={ toggleId }
- className="components-toggle-control__label"
- >
- { field.label }
- </FlexBlock>
- </HStack>
- </BaseControl>
+ <div className="woocommerce-schedule-sale-control">
+ <CheckboxControl
+ label={ field.label }
+ checked={ checked }
+ onChange={ handleScheduleChange }
+ />
+ { checked && (
+ <div className="woocommerce-schedule-sale-control__dates">
+ <DatePicker
+ data={ data }
+ onChange={ handleDateOnSaleFromChange }
+ field={ {
+ label: __( 'Start sale on', 'woocommerce' ),
+ } }
+ fieldKey="date_on_sale_from"
+ min={ today }
+ />
+ <DatePicker
+ data={ data }
+ onChange={ onChange }
+ field={ {
+ label: __( 'End sale on', 'woocommerce' ),
+ } }
+ fieldKey="date_on_sale_to"
+ min={ minDateOnSaleTo }
+ />
+ </div>
+ ) }
+ </div>
);
},
+ getValue: ( { item } ) =>
+ !! item.date_on_sale_to || !! item.date_on_sale_from,
+ setValue: ( { item, value } ) => {
+ if ( ! value ) {
+ return {
+ date_on_sale_from: '',
+ date_on_sale_to: '',
+ };
+ }
+
+ let dateOnSaleFrom = item.date_on_sale_from || '';
+ const dateOnSaleTo = item.date_on_sale_to || '';
+
+ if ( ! dateOnSaleFrom && ! dateOnSaleTo ) {
+ dateOnSaleFrom = getLocalDefaultSaleStart();
+ }
+
+ return {
+ date_on_sale_from: dateOnSaleFrom,
+ date_on_sale_to: dateOnSaleTo,
+ };
+ },
};
diff --git a/packages/js/experimental-products-app/src/fields/schedule_sale/style.scss b/packages/js/experimental-products-app/src/fields/schedule_sale/style.scss
new file mode 100644
index 00000000000..4c5d879e56f
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/schedule_sale/style.scss
@@ -0,0 +1,11 @@
+.woocommerce-schedule-sale-control {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.woocommerce-schedule-sale-control__dates {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 16px;
+}
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 f206be358bd..cd70129e0ab 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
@@ -592,8 +592,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
];
const basePriceFieldIds = [ 'regular_price', 'sale_price' ];
@@ -635,8 +633,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
'images',
'sku',
@@ -690,8 +686,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
] );
} );
@@ -982,8 +976,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
'images',
'sku',
@@ -1025,8 +1017,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
'stock',
'manage_stock',
@@ -1106,8 +1096,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- 'date_on_sale_from',
- 'date_on_sale_to',
'cost_of_goods_sold',
...bulkSellableInstanceFieldIds,
] )
@@ -1377,7 +1365,7 @@ describe( 'product edit utils', () => {
[ 'simple product', 'simple' ],
[ 'variation product', 'variation' ],
] as const )(
- 'groups sale schedule date fields on the same row for %s',
+ 'uses schedule sale as a compound price field for %s',
( _label, productType ) => {
const product = buildProduct( {
type: productType,
@@ -1398,14 +1386,6 @@ describe( 'product edit utils', () => {
'regular_price',
'sale_price',
'schedule_sale',
- {
- id: 'sale-schedule-dates',
- layout: { type: 'row' },
- children: [
- 'date_on_sale_from',
- 'date_on_sale_to',
- ],
- },
'cost_of_goods_sold',
],
} );
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 b0ec4f27f82..404db201bc4 100644
--- a/packages/js/experimental-products-app/src/product-edit/utils.ts
+++ b/packages/js/experimental-products-app/src/product-edit/utils.ts
@@ -120,11 +120,6 @@ const SIMPLE_PRODUCT_EDIT_FORM_FIELDS = [
'regular_price',
'sale_price',
'schedule_sale',
- {
- id: 'sale-schedule-dates',
- layout: { type: 'row' as const },
- children: [ 'date_on_sale_from', 'date_on_sale_to' ],
- },
'cost_of_goods_sold',
] ),
createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
@@ -158,11 +153,6 @@ const VARIATION_PRODUCT_EDIT_FORM_FIELDS = [
'regular_price',
'sale_price',
'schedule_sale',
- {
- id: 'sale-schedule-dates',
- layout: { type: 'row' as const },
- children: [ 'date_on_sale_from', 'date_on_sale_to' ],
- },
'cost_of_goods_sold',
] ),
createProductEditFormGroup( 'image-fields', __( 'Images', 'woocommerce' ), [
@@ -217,11 +207,6 @@ const EXTERNAL_PRODUCT_EDIT_FORM_FIELDS = [
'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',
diff --git a/packages/js/experimental-products-app/src/style.scss b/packages/js/experimental-products-app/src/style.scss
index 10fd41274e4..5623223a094 100644
--- a/packages/js/experimental-products-app/src/style.scss
+++ b/packages/js/experimental-products-app/src/style.scss
@@ -29,6 +29,7 @@
@import "./fields/components/taxonomy-edit/style.scss";
@import "./fields/downloadable/style.scss";
@import "./fields/images/style.scss";
+ @import "./fields/schedule_sale/style.scss";
@import "./fields/stock/style.scss";
@import "./fields/visibility_summary/style.scss";
--wp-ui-drawer-z-index: 100000;