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:
Maikel Perez 2024-02-14 13:57:53 -03:00 committed by GitHub
parent 4061bbfc2b
commit 5af88543eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 257 additions and 11 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add the collapsible Schedule section

View File

@ -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,

View File

@ -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' }
/> />
); );

View File

@ -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>
); );
} }

View File

@ -0,0 +1,2 @@
export * from './schedule-section';
export * from './types';

View File

@ -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>
);
}

View File

@ -0,0 +1,3 @@
export type ScheduleSectionProps = {
postType: string;
};

View File

@ -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';

View File

@ -0,0 +1 @@
export * from './use-product-scheduled';

View File

@ -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 );
}

View File

@ -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 }`;
}

View File

@ -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';

View File

@ -0,0 +1,7 @@
export function isSameDay( left: Date, right: Date ) {
return (
left.getDate() === right.getDate() &&
left.getMonth() === right.getMonth() &&
left.getFullYear() === right.getFullYear()
);
}

View File

@ -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( '' )
);
}

View File

@ -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;
}

View File

@ -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';