Add the collapsible Schedule section (#44563)
* Create schedule section within the pre-publish panel component * Add publish date time picket to the schedule section * Enhance schedule section title to show the selected date * Change the text of the publish button to schedule once the product is scheduled * Add changelog file * Fix linter errors * Set schedule text in pre publish button too * Fix timezone offset from getSiteSettingsTimezoneAbbreviation when the onboarding wizard is skipped
This commit is contained in:
parent
4061bbfc2b
commit
5af88543eb
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add the collapsible Schedule section
|
|
@ -14,6 +14,7 @@ import { MouseEvent } from 'react';
|
||||||
import { useValidations } from '../../../../contexts/validation-context';
|
import { useValidations } from '../../../../contexts/validation-context';
|
||||||
import type { WPError } from '../../../../utils/get-product-error-message';
|
import type { WPError } from '../../../../utils/get-product-error-message';
|
||||||
import type { PublishButtonProps } from '../../publish-button';
|
import type { PublishButtonProps } from '../../publish-button';
|
||||||
|
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
|
||||||
|
|
||||||
export function usePublish( {
|
export function usePublish( {
|
||||||
productType = 'product',
|
productType = 'product',
|
||||||
|
@ -35,6 +36,8 @@ export function usePublish( {
|
||||||
'id'
|
'id'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isScheduled = useProductScheduled( productType );
|
||||||
|
|
||||||
const { isSaving, isDirty } = useSelect(
|
const { isSaving, isDirty } = useSelect(
|
||||||
( select ) => {
|
( select ) => {
|
||||||
const {
|
const {
|
||||||
|
@ -42,8 +45,6 @@ export function usePublish( {
|
||||||
isSavingEntityRecord,
|
isSavingEntityRecord,
|
||||||
// @ts-expect-error There are no types for this.
|
// @ts-expect-error There are no types for this.
|
||||||
hasEditsForEntityRecord,
|
hasEditsForEntityRecord,
|
||||||
// @ts-expect-error There are no types for this.
|
|
||||||
getRawEntityRecord,
|
|
||||||
} = select( 'core' );
|
} = select( 'core' );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -57,11 +58,6 @@ export function usePublish( {
|
||||||
productType,
|
productType,
|
||||||
productId
|
productId
|
||||||
),
|
),
|
||||||
currentPost: getRawEntityRecord< boolean >(
|
|
||||||
'postType',
|
|
||||||
productType,
|
|
||||||
productId
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[ productId ]
|
[ productId ]
|
||||||
|
@ -146,10 +142,18 @@ export function usePublish( {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
function getButtonText() {
|
||||||
children: isPublished
|
if ( isScheduled ) {
|
||||||
|
return __( 'Schedule', 'woocommerce' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPublished
|
||||||
? __( 'Update', 'woocommerce' )
|
? __( 'Update', 'woocommerce' )
|
||||||
: __( 'Publish', 'woocommerce' ),
|
: __( 'Publish', 'woocommerce' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
children: getButtonText(),
|
||||||
...props,
|
...props,
|
||||||
isBusy,
|
isBusy,
|
||||||
'aria-disabled': isDisabled,
|
'aria-disabled': isDisabled,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
||||||
import { PrepublishButtonProps } from './types';
|
import { PrepublishButtonProps } from './types';
|
||||||
import { useValidations } from '../../contexts/validation-context';
|
import { useValidations } from '../../contexts/validation-context';
|
||||||
import { TRACKS_SOURCE } from '../../constants';
|
import { TRACKS_SOURCE } from '../../constants';
|
||||||
|
import { useProductScheduled } from '../../hooks/use-product-scheduled';
|
||||||
|
|
||||||
export function PrepublishButton( {
|
export function PrepublishButton( {
|
||||||
productId,
|
productId,
|
||||||
|
@ -49,6 +50,7 @@ export function PrepublishButton( {
|
||||||
|
|
||||||
const isBusy = isSaving || isValidating;
|
const isBusy = isSaving || isValidating;
|
||||||
const isDisabled = isBusy || ! isDirty;
|
const isDisabled = isBusy || ! isDirty;
|
||||||
|
const isScheduled = useProductScheduled( productType );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -61,7 +63,11 @@ export function PrepublishButton( {
|
||||||
} }
|
} }
|
||||||
isBusy={ isBusy }
|
isBusy={ isBusy }
|
||||||
aria-disabled={ isDisabled }
|
aria-disabled={ isDisabled }
|
||||||
children={ __( 'Publish', 'woocommerce' ) }
|
children={
|
||||||
|
isScheduled
|
||||||
|
? __( 'Schedule', 'woocommerce' )
|
||||||
|
: __( 'Publish', 'woocommerce' )
|
||||||
|
}
|
||||||
variant={ 'primary' }
|
variant={ 'primary' }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { PrepublishPanelProps } from './types';
|
||||||
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
||||||
import { TRACKS_SOURCE } from '../../constants';
|
import { TRACKS_SOURCE } from '../../constants';
|
||||||
import { VisibilitySection } from './visibility-section';
|
import { VisibilitySection } from './visibility-section';
|
||||||
|
import { ScheduleSection } from './schedule-section';
|
||||||
|
|
||||||
export function PrepublishPanel( {
|
export function PrepublishPanel( {
|
||||||
productId,
|
productId,
|
||||||
|
@ -60,6 +61,8 @@ export function PrepublishPanel( {
|
||||||
<span>{ description }</span>
|
<span>{ description }</span>
|
||||||
</div>
|
</div>
|
||||||
<VisibilitySection productType={ productType } />
|
<VisibilitySection productType={ productType } />
|
||||||
|
|
||||||
|
<ScheduleSection postType={ productType } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './schedule-section';
|
||||||
|
export * from './types';
|
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { PanelBody } from '@wordpress/components';
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import {
|
||||||
|
DateSettings,
|
||||||
|
dateI18n,
|
||||||
|
getDate,
|
||||||
|
__experimentalGetSettings as getSettings,
|
||||||
|
} from '@wordpress/date';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { __, _x, isRTL, sprintf } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error no exported member
|
||||||
|
__experimentalPublishDateTimePicker as PublishDateTimePicker,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ScheduleSectionProps } from './types';
|
||||||
|
import {
|
||||||
|
getSiteSettingsTimezoneAbbreviation,
|
||||||
|
isSameDay,
|
||||||
|
isSiteSettingsTime12HourFormatted,
|
||||||
|
isSiteSettingsTimezoneSameAsDateTimezone,
|
||||||
|
} from '../../../utils';
|
||||||
|
|
||||||
|
export function getFormattedDateTime( value: string ) {
|
||||||
|
const { formats } = getSettings() as DateSettings;
|
||||||
|
|
||||||
|
return dateI18n(
|
||||||
|
sprintf(
|
||||||
|
// translators: %s: Time of day the product is scheduled for.
|
||||||
|
_x(
|
||||||
|
'F j, Y %s',
|
||||||
|
'product schedule full date format',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
formats.time
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFullScheduleLabel( dateAttribute: string ) {
|
||||||
|
const timezoneAbbreviation = getSiteSettingsTimezoneAbbreviation();
|
||||||
|
const formattedDate = getFormattedDateTime( dateAttribute );
|
||||||
|
|
||||||
|
return isRTL()
|
||||||
|
? `${ timezoneAbbreviation } ${ formattedDate }`
|
||||||
|
: `${ formattedDate } ${ timezoneAbbreviation }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScheduleLabel( dateAttribute?: string, now = new Date() ) {
|
||||||
|
if ( ! dateAttribute ) {
|
||||||
|
return __( 'Immediately', 'woocommerce' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user timezone does not equal the site timezone then using words
|
||||||
|
// like 'tomorrow' is confusing, so show the full date.
|
||||||
|
if ( ! isSiteSettingsTimezoneSameAsDateTimezone( now ) ) {
|
||||||
|
return getFullScheduleLabel( dateAttribute );
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formats } = getSettings() as DateSettings;
|
||||||
|
const date = getDate( dateAttribute );
|
||||||
|
|
||||||
|
if ( isSameDay( date, now ) ) {
|
||||||
|
return sprintf(
|
||||||
|
// translators: %s: Time of day the product is scheduled for.
|
||||||
|
__( 'Today at %s', 'woocommerce' ),
|
||||||
|
dateI18n( formats.time, dateAttribute, undefined )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tomorrow = new Date( now );
|
||||||
|
tomorrow.setDate( tomorrow.getDate() + 1 );
|
||||||
|
|
||||||
|
if ( isSameDay( date, tomorrow ) ) {
|
||||||
|
return sprintf(
|
||||||
|
// translators: %s: Time of day the product is scheduled for.
|
||||||
|
__( 'Tomorrow at %s', 'woocommerce' ),
|
||||||
|
dateI18n( formats.time, dateAttribute, undefined )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( date.getFullYear() === now.getFullYear() ) {
|
||||||
|
return dateI18n(
|
||||||
|
sprintf(
|
||||||
|
// translators: %s: Time of day the product is scheduled for.
|
||||||
|
_x(
|
||||||
|
'F j %s',
|
||||||
|
'product schedule date format without year',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
formats.time
|
||||||
|
),
|
||||||
|
date,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFormattedDateTime( dateAttribute );
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScheduleSection( { postType }: ScheduleSectionProps ) {
|
||||||
|
const [ editedDate, setDate, date ] = useEntityProp< string >(
|
||||||
|
'postType',
|
||||||
|
postType,
|
||||||
|
'date_created'
|
||||||
|
);
|
||||||
|
|
||||||
|
function handlePublishDateTimePickerChange( value: string ) {
|
||||||
|
setDate( value );
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelBody
|
||||||
|
initialOpen={ false }
|
||||||
|
// @ts-expect-error title does currently support this value
|
||||||
|
title={ [
|
||||||
|
__( 'Add:', 'woocommerce' ),
|
||||||
|
<span className="editor-post-publish-panel__link" key="label">
|
||||||
|
{ getScheduleLabel(
|
||||||
|
editedDate === date ? undefined : editedDate
|
||||||
|
) }
|
||||||
|
</span>,
|
||||||
|
] }
|
||||||
|
>
|
||||||
|
<PublishDateTimePicker
|
||||||
|
currentDate={ editedDate }
|
||||||
|
onChange={ handlePublishDateTimePickerChange }
|
||||||
|
is12Hour={ isSiteSettingsTime12HourFormatted() }
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type ScheduleSectionProps = {
|
||||||
|
postType: string;
|
||||||
|
};
|
|
@ -6,3 +6,4 @@ export { useVariationSwitcher as __experimentalUseVariationSwitcher } from './us
|
||||||
export { default as __experimentalUseProductEntityProp } from './use-product-entity-prop';
|
export { default as __experimentalUseProductEntityProp } from './use-product-entity-prop';
|
||||||
export { default as __experimentalUseProductMetadata } from './use-product-metadata';
|
export { default as __experimentalUseProductMetadata } from './use-product-metadata';
|
||||||
export { useProductTemplate as __experimentalUseProductTemplate } from './use-product-template';
|
export { useProductTemplate as __experimentalUseProductTemplate } from './use-product-template';
|
||||||
|
export { useProductScheduled as __experimentalUseProductScheduled } from './use-product-scheduled';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './use-product-scheduled';
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
|
import { isInTheFuture } from '@wordpress/date';
|
||||||
|
|
||||||
|
export function useProductScheduled( postType: string ) {
|
||||||
|
const [ date ] = useEntityProp< string >(
|
||||||
|
'postType',
|
||||||
|
postType,
|
||||||
|
'date_created'
|
||||||
|
);
|
||||||
|
|
||||||
|
return isInTheFuture( date );
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
TimezoneConfig,
|
||||||
|
__experimentalGetSettings as getSettings,
|
||||||
|
} from '@wordpress/date';
|
||||||
|
|
||||||
|
export function getSiteSettingsTimezoneAbbreviation() {
|
||||||
|
const { timezone } = getSettings() as {
|
||||||
|
timezone: TimezoneConfig & { offsetFormatted: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( timezone.abbr && isNaN( Number( timezone.abbr ) ) ) {
|
||||||
|
return timezone.abbr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = Number( timezone.offset ) < 0 ? '' : '+';
|
||||||
|
return `UTC${ symbol }${ timezone.offsetFormatted ?? timezone.offset }`;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './get-site-settings-timezone-abbreviation';
|
||||||
|
export * from './is-same-day';
|
||||||
|
export * from './is-site-settings-time-12-hour-formatted';
|
||||||
|
export * from './is-site-settings-timezone-same-as-date-timezone';
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function isSameDay( left: Date, right: Date ) {
|
||||||
|
return (
|
||||||
|
left.getDate() === right.getDate() &&
|
||||||
|
left.getMonth() === right.getMonth() &&
|
||||||
|
left.getFullYear() === right.getFullYear()
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
DateSettings,
|
||||||
|
__experimentalGetSettings as getSettings,
|
||||||
|
} from '@wordpress/date';
|
||||||
|
|
||||||
|
export function isSiteSettingsTime12HourFormatted() {
|
||||||
|
const settings = getSettings() as DateSettings;
|
||||||
|
|
||||||
|
return /a(?!\\)/i.test(
|
||||||
|
settings.formats.time
|
||||||
|
.toLowerCase()
|
||||||
|
.replace( /\\\\/g, '' )
|
||||||
|
.split( '' )
|
||||||
|
.reverse()
|
||||||
|
.join( '' )
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
DateSettings,
|
||||||
|
__experimentalGetSettings as getSettings,
|
||||||
|
} from '@wordpress/date';
|
||||||
|
|
||||||
|
export function isSiteSettingsTimezoneSameAsDateTimezone( date: Date ) {
|
||||||
|
const { timezone } = getSettings() as DateSettings;
|
||||||
|
|
||||||
|
const siteOffset = Number( timezone.offset );
|
||||||
|
const dateOffset = -1 * ( date.getTimezoneOffset() / 60 );
|
||||||
|
return siteOffset === dateOffset;
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import { hasAttributesUsedForVariations } from './has-attributes-used-for-variat
|
||||||
import { isValidEmail } from './validate-email';
|
import { isValidEmail } from './validate-email';
|
||||||
|
|
||||||
export * from './create-ordered-children';
|
export * from './create-ordered-children';
|
||||||
|
export * from './date';
|
||||||
export * from './sort-fills-by-order';
|
export * from './sort-fills-by-order';
|
||||||
export * from './register-product-editor-block-type';
|
export * from './register-product-editor-block-type';
|
||||||
export * from './init-block';
|
export * from './init-block';
|
||||||
|
|
Loading…
Reference in New Issue