Add menu item to publish button with 'Move to trash' (#44940)
* Move products to trash * Create useProductManager hook * Use useProductManager hook in the publish button * Create PublishButtonMenu * Hide move to trash when the product is already in trash * Fix linter error * Add changelog file
This commit is contained in:
parent
5d808ad9fc
commit
d0f095df72
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add menu item to publish button with 'Move to trash'
|
|
@ -13,11 +13,9 @@ import type { ButtonWithDropdownMenuProps } from './types';
|
|||
|
||||
export * from './types';
|
||||
|
||||
export const ButtonWithDropdownMenu: React.FC<
|
||||
ButtonWithDropdownMenuProps
|
||||
> = ( {
|
||||
export function ButtonWithDropdownMenu( {
|
||||
dropdownButtonLabel = __( 'More options', 'woocommerce' ),
|
||||
controls = [],
|
||||
controls,
|
||||
defaultOpen = false,
|
||||
popoverProps: {
|
||||
placement = 'bottom-end',
|
||||
|
@ -29,8 +27,9 @@ export const ButtonWithDropdownMenu: React.FC<
|
|||
offset: 0,
|
||||
},
|
||||
className,
|
||||
renderMenu,
|
||||
...props
|
||||
} ) => {
|
||||
}: ButtonWithDropdownMenuProps ) {
|
||||
return (
|
||||
<Flex
|
||||
className={ `woocommerce-button-with-dropdown-menu${
|
||||
|
@ -65,8 +64,10 @@ export const ButtonWithDropdownMenu: React.FC<
|
|||
offset,
|
||||
} }
|
||||
defaultOpen={ defaultOpen }
|
||||
/>
|
||||
>
|
||||
{ renderMenu }
|
||||
</DropdownMenu>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import type {
|
||||
Dropdown,
|
||||
// @ts-expect-error no exported member.
|
||||
DropdownOption,
|
||||
} from '@wordpress/components';
|
||||
|
@ -44,4 +45,5 @@ export type ButtonWithDropdownMenuProps = Omit<
|
|||
defaultOpen?: boolean;
|
||||
controls?: DropdownOption[];
|
||||
popoverProps?: PopoverProps;
|
||||
renderMenu?( props: Dropdown.RenderProps ): React.ReactElement;
|
||||
};
|
||||
|
|
|
@ -4,18 +4,17 @@
|
|||
import { MouseEvent } from 'react';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import type { Product, ProductVariation } from '@woocommerce/data';
|
||||
import type { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../../../../contexts/validation-context';
|
||||
import { useProductManager } from '../../../../hooks/use-product-manager';
|
||||
import type { WPError } from '../../../../utils/get-product-error-message';
|
||||
import type { PublishButtonProps } from '../../publish-button';
|
||||
|
||||
export function usePublish( {
|
||||
export function usePublish< T = Product >( {
|
||||
productType = 'product',
|
||||
disabled,
|
||||
onClick,
|
||||
|
@ -23,20 +22,11 @@ export function usePublish( {
|
|||
onPublishError,
|
||||
...props
|
||||
}: PublishButtonProps & {
|
||||
onPublishSuccess?( product: Product ): void;
|
||||
onPublishSuccess?( product: T ): void;
|
||||
onPublishError?( error: WPError ): void;
|
||||
} ): Button.ButtonProps & {
|
||||
publish(
|
||||
productOrVariation?: Partial< Product | ProductVariation >
|
||||
): Promise< Product | ProductVariation | undefined >;
|
||||
} {
|
||||
const { isValidating, validate } = useValidations< Product >();
|
||||
|
||||
const [ productId ] = useEntityProp< number >(
|
||||
'postType',
|
||||
productType,
|
||||
'id'
|
||||
);
|
||||
} ): Button.ButtonProps {
|
||||
const { isValidating, isDirty, isPublishing, publish } =
|
||||
useProductManager( productType );
|
||||
|
||||
const [ status, , prevStatus ] = useEntityProp< Product[ 'status' ] >(
|
||||
'postType',
|
||||
|
@ -44,97 +34,10 @@ export function usePublish( {
|
|||
'status'
|
||||
);
|
||||
|
||||
const { isSaving, isDirty } = useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
// @ts-expect-error There are no types for this.
|
||||
isSavingEntityRecord,
|
||||
// @ts-expect-error There are no types for this.
|
||||
hasEditsForEntityRecord,
|
||||
} = select( 'core' );
|
||||
|
||||
return {
|
||||
isSaving: isSavingEntityRecord< boolean >(
|
||||
'postType',
|
||||
productType,
|
||||
productId
|
||||
),
|
||||
isDirty: hasEditsForEntityRecord(
|
||||
'postType',
|
||||
productType,
|
||||
productId
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const isBusy = isSaving || isValidating;
|
||||
const isBusy = isPublishing || isValidating;
|
||||
const isDisabled = disabled || isBusy || ! isDirty;
|
||||
|
||||
// @ts-expect-error There are no types for this.
|
||||
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
|
||||
|
||||
async function publish(
|
||||
productOrVariation: Partial< Product | ProductVariation > = {}
|
||||
) {
|
||||
const isPublished = status === 'publish' || status === 'future';
|
||||
|
||||
try {
|
||||
// The publish button click not only change the status of the product
|
||||
// but also save all the pending changes. So even if the status is
|
||||
// publish it's possible to save the product too.
|
||||
const data = ! isPublished
|
||||
? { status: 'publish', ...productOrVariation }
|
||||
: productOrVariation;
|
||||
|
||||
await validate( data as Partial< Product > );
|
||||
|
||||
await editEntityRecord( 'postType', productType, productId, data );
|
||||
|
||||
const publishedProduct = await saveEditedEntityRecord<
|
||||
Product | ProductVariation
|
||||
>( 'postType', productType, productId, {
|
||||
throwOnError: true,
|
||||
} );
|
||||
|
||||
if ( publishedProduct && onPublishSuccess ) {
|
||||
onPublishSuccess( publishedProduct );
|
||||
}
|
||||
|
||||
return publishedProduct as Product | ProductVariation;
|
||||
} catch ( error ) {
|
||||
if ( onPublishError ) {
|
||||
let wpError = error as WPError;
|
||||
if ( ! wpError.code ) {
|
||||
wpError = {
|
||||
code: isPublished
|
||||
? 'product_publish_error'
|
||||
: 'product_create_error',
|
||||
} as WPError;
|
||||
if ( ( error as Record< string, string > ).variations ) {
|
||||
wpError.code = 'variable_product_no_variation_prices';
|
||||
wpError.message = (
|
||||
error as Record< string, string >
|
||||
).variations;
|
||||
} else {
|
||||
const errorMessage = Object.values(
|
||||
error as Record< string, string >
|
||||
).find( ( value ) => value !== undefined ) as
|
||||
| string
|
||||
| undefined;
|
||||
if ( errorMessage !== undefined ) {
|
||||
wpError.code = 'product_form_field_error';
|
||||
wpError.message = errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
onPublishError( wpError );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClick( event: MouseEvent< HTMLButtonElement > ) {
|
||||
function handleClick( event: MouseEvent< HTMLButtonElement > ) {
|
||||
if ( isDisabled ) {
|
||||
event.preventDefault?.();
|
||||
return;
|
||||
|
@ -144,7 +47,7 @@ export function usePublish( {
|
|||
onClick( event );
|
||||
}
|
||||
|
||||
await publish();
|
||||
publish().then( onPublishSuccess ).catch( onPublishError );
|
||||
}
|
||||
|
||||
function getButtonText() {
|
||||
|
@ -169,6 +72,5 @@ export function usePublish( {
|
|||
'aria-disabled': isDisabled,
|
||||
variant: 'primary',
|
||||
onClick: handleClick,
|
||||
publish,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './publish-button-menu';
|
||||
export * from './types';
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement, Fragment, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import type { ProductStatus } from '@woocommerce/data';
|
||||
import { navigateTo } from '@woocommerce/navigation';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductManager } from '../../../../hooks/use-product-manager';
|
||||
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
|
||||
import { recordProductEvent } from '../../../../utils/record-product-event';
|
||||
import { getProductErrorMessage } from '../../../../utils/get-product-error-message';
|
||||
import { ButtonWithDropdownMenu } from '../../../button-with-dropdown-menu';
|
||||
import { SchedulePublishModal } from '../../../schedule-publish-modal';
|
||||
import { showSuccessNotice } from '../utils';
|
||||
import type { PublishButtonMenuProps } from './types';
|
||||
|
||||
export function PublishButtonMenu( {
|
||||
postType,
|
||||
...props
|
||||
}: PublishButtonMenuProps ) {
|
||||
const { isScheduled, schedule, date, formattedDate } =
|
||||
useProductScheduled( postType );
|
||||
const [ showScheduleModal, setShowScheduleModal ] = useState<
|
||||
'schedule' | 'edit' | undefined
|
||||
>();
|
||||
const { trash } = useProductManager( postType );
|
||||
const { createErrorNotice, createSuccessNotice } =
|
||||
useDispatch( 'core/notices' );
|
||||
const [ , , prevStatus ] = useEntityProp< ProductStatus >(
|
||||
'postType',
|
||||
postType,
|
||||
'status'
|
||||
);
|
||||
|
||||
function scheduleProduct( dateString?: string ) {
|
||||
schedule( dateString )
|
||||
.then( ( scheduledProduct ) => {
|
||||
recordProductEvent( 'product_schedule', scheduledProduct );
|
||||
|
||||
showSuccessNotice( scheduledProduct );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
const message = getProductErrorMessage( error );
|
||||
createErrorNotice( message );
|
||||
} )
|
||||
.finally( () => {
|
||||
setShowScheduleModal( undefined );
|
||||
} );
|
||||
}
|
||||
|
||||
function renderSchedulePublishModal() {
|
||||
return (
|
||||
showScheduleModal && (
|
||||
<SchedulePublishModal
|
||||
postType={ postType }
|
||||
value={ showScheduleModal === 'edit' ? date : undefined }
|
||||
onCancel={ () => setShowScheduleModal( undefined ) }
|
||||
onSchedule={ scheduleProduct }
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderMenu( { onClose }: Dropdown.RenderProps ) {
|
||||
return (
|
||||
<>
|
||||
<MenuGroup>
|
||||
{ isScheduled ? (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
scheduleProduct();
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Publish now', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
info={ formattedDate }
|
||||
onClick={ () => {
|
||||
setShowScheduleModal( 'edit' );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Edit schedule', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
setShowScheduleModal( 'schedule' );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Schedule publish', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
</MenuGroup>
|
||||
|
||||
{ prevStatus !== 'trash' && (
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
onClick={ () => {
|
||||
trash()
|
||||
.then( ( deletedProduct ) => {
|
||||
recordProductEvent(
|
||||
'product_delete',
|
||||
deletedProduct
|
||||
);
|
||||
createSuccessNotice(
|
||||
__(
|
||||
'Product successfully deleted',
|
||||
'woocommerce'
|
||||
)
|
||||
);
|
||||
const productListUrl = getAdminLink(
|
||||
'edit.php?post_type=product'
|
||||
);
|
||||
navigateTo( {
|
||||
url: productListUrl,
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
const message =
|
||||
getProductErrorMessage( error );
|
||||
createErrorNotice( message );
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Move to trash', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonWithDropdownMenu { ...props } renderMenu={ renderMenu } />
|
||||
|
||||
{ renderSchedulePublishModal() }
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ButtonWithDropdownMenuProps } from '../../../button-with-dropdown-menu';
|
||||
|
||||
export type PublishButtonMenuProps = ButtonWithDropdownMenuProps & {
|
||||
postType: string;
|
||||
};
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { MouseEvent, useState } from 'react';
|
||||
import { Button } from '@wordpress/components';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { Button, Dropdown } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { dispatch, useDispatch } from '@wordpress/data';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { type Product } from '@woocommerce/data';
|
||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
@ -19,58 +18,10 @@ import { getProductErrorMessage } from '../../../utils/get-product-error-message
|
|||
import { recordProductEvent } from '../../../utils/record-product-event';
|
||||
import { useFeedbackBar } from '../../../hooks/use-feedback-bar';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { ButtonWithDropdownMenu } from '../../button-with-dropdown-menu';
|
||||
import { usePublish } from '../hooks/use-publish';
|
||||
import { PublishButtonProps } from './types';
|
||||
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
|
||||
import { SchedulePublishModal } from '../../schedule-publish-modal';
|
||||
import { formatScheduleDatetime } from '../../../utils';
|
||||
|
||||
function getNoticeContent( product: Product, prevStatus: Product[ 'status' ] ) {
|
||||
if (
|
||||
window.wcAdminFeatures[ 'product-pre-publish-modal' ] &&
|
||||
product.status === 'future'
|
||||
) {
|
||||
return sprintf(
|
||||
// translators: %s: The datetime the product is scheduled for.
|
||||
__( 'Product scheduled for %s.', 'woocommerce' ),
|
||||
formatScheduleDatetime( product.date_created )
|
||||
);
|
||||
}
|
||||
|
||||
if ( prevStatus === 'publish' || prevStatus === 'future' ) {
|
||||
return __( 'Product updated.', 'woocommerce' );
|
||||
}
|
||||
|
||||
return __( 'Product published.', 'woocommerce' );
|
||||
}
|
||||
|
||||
function showSuccessNotice(
|
||||
product: Product,
|
||||
prevStatus: Product[ 'status' ]
|
||||
) {
|
||||
const { createSuccessNotice } = dispatch( 'core/notices' );
|
||||
|
||||
const noticeContent = getNoticeContent( product, prevStatus );
|
||||
const noticeOptions = {
|
||||
icon: '🎉',
|
||||
actions: [
|
||||
{
|
||||
label: __( 'View in store', 'woocommerce' ),
|
||||
// Leave the url to support a11y.
|
||||
url: product.permalink,
|
||||
onClick( event: MouseEvent< HTMLAnchorElement > ) {
|
||||
event.preventDefault();
|
||||
// Notice actions do not support target anchor prop,
|
||||
// so this forces the page to be opened in a new tab.
|
||||
window.open( product.permalink, '_blank' );
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
createSuccessNotice( noticeContent, noticeOptions );
|
||||
}
|
||||
import { PublishButtonMenu } from './publish-button-menu';
|
||||
import { showSuccessNotice } from './utils';
|
||||
import type { PublishButtonProps } from './types';
|
||||
|
||||
export function PublishButton( {
|
||||
productType = 'product',
|
||||
|
@ -87,7 +38,7 @@ export function PublishButton( {
|
|||
'status'
|
||||
);
|
||||
|
||||
const { publish, ...publishButtonProps } = usePublish( {
|
||||
const publishButtonProps = usePublish( {
|
||||
productType,
|
||||
...props,
|
||||
onPublishSuccess( savedProduct: Product ) {
|
||||
|
@ -114,70 +65,16 @@ export function PublishButton( {
|
|||
},
|
||||
} );
|
||||
|
||||
const { isScheduled, schedule, date, formattedDate } =
|
||||
useProductScheduled( productType );
|
||||
const [ showScheduleModal, setShowScheduleModal ] = useState<
|
||||
'schedule' | 'edit' | undefined
|
||||
>();
|
||||
|
||||
if (
|
||||
productType === 'product' &&
|
||||
window.wcAdminFeatures[ 'product-pre-publish-modal' ] &&
|
||||
prePublish
|
||||
) {
|
||||
function getPublishButtonControls() {
|
||||
return [
|
||||
isScheduled
|
||||
? [
|
||||
{
|
||||
title: __( 'Publish now', 'woocommerce' ),
|
||||
async onClick() {
|
||||
await schedule( publish );
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="woocommerce-product-header__actions-edit-schedule">
|
||||
<div>
|
||||
{ __(
|
||||
'Edit schedule',
|
||||
'woocommerce'
|
||||
) }
|
||||
</div>
|
||||
<div>{ formattedDate }</div>
|
||||
</div>
|
||||
),
|
||||
onClick() {
|
||||
setShowScheduleModal( 'edit' );
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: __( 'Schedule publish', 'woocommerce' ),
|
||||
onClick() {
|
||||
setShowScheduleModal( 'schedule' );
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function renderSchedulePublishModal() {
|
||||
function renderPublishButtonMenu(
|
||||
menuProps: Dropdown.RenderProps
|
||||
): React.ReactElement {
|
||||
return (
|
||||
showScheduleModal && (
|
||||
<SchedulePublishModal
|
||||
postType={ productType }
|
||||
value={
|
||||
showScheduleModal === 'edit' ? date : undefined
|
||||
}
|
||||
onCancel={ () => setShowScheduleModal( undefined ) }
|
||||
onSchedule={ async ( value ) => {
|
||||
await schedule( publish, value );
|
||||
setShowScheduleModal( undefined );
|
||||
} }
|
||||
/>
|
||||
)
|
||||
<PublishButtonMenu { ...menuProps } postType={ productType } />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -198,27 +95,23 @@ export function PublishButton( {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonWithDropdownMenu
|
||||
{ ...publishButtonProps }
|
||||
onClick={ handlePrePublishButtonClick }
|
||||
controls={ getPublishButtonControls() }
|
||||
/>
|
||||
|
||||
{ renderSchedulePublishModal() }
|
||||
</>
|
||||
<PublishButtonMenu
|
||||
{ ...publishButtonProps }
|
||||
postType={ productType }
|
||||
controls={ undefined }
|
||||
onClick={ handlePrePublishButtonClick }
|
||||
renderMenu={ renderPublishButtonMenu }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonWithDropdownMenu
|
||||
{ ...publishButtonProps }
|
||||
controls={ getPublishButtonControls() }
|
||||
/>
|
||||
|
||||
{ renderSchedulePublishModal() }
|
||||
</>
|
||||
<PublishButtonMenu
|
||||
{ ...publishButtonProps }
|
||||
postType={ productType }
|
||||
controls={ undefined }
|
||||
renderMenu={ renderPublishButtonMenu }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './show-success-notice';
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import type { Product, ProductStatus } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatScheduleDatetime } from '../../../../utils';
|
||||
|
||||
function getNoticeContent( product: Product, prevStatus?: ProductStatus ) {
|
||||
if (
|
||||
window.wcAdminFeatures[ 'product-pre-publish-modal' ] &&
|
||||
product.status === 'future'
|
||||
) {
|
||||
return sprintf(
|
||||
// translators: %s: The datetime the product is scheduled for.
|
||||
__( 'Product scheduled for %s.', 'woocommerce' ),
|
||||
formatScheduleDatetime( `${ product.date_created_gmt }+00:00` )
|
||||
);
|
||||
}
|
||||
|
||||
if ( prevStatus === 'publish' || prevStatus === 'future' ) {
|
||||
return __( 'Product updated.', 'woocommerce' );
|
||||
}
|
||||
|
||||
return __( 'Product published.', 'woocommerce' );
|
||||
}
|
||||
|
||||
export function showSuccessNotice(
|
||||
product: Product,
|
||||
prevStatus?: ProductStatus
|
||||
) {
|
||||
const { createSuccessNotice } = dispatch( 'core/notices' );
|
||||
|
||||
const noticeContent = getNoticeContent( product, prevStatus );
|
||||
const noticeOptions = {
|
||||
icon: '🎉',
|
||||
actions: [
|
||||
{
|
||||
label: __( 'View in store', 'woocommerce' ),
|
||||
// Leave the url to support a11y.
|
||||
url: product.permalink,
|
||||
onClick( event: React.MouseEvent< HTMLAnchorElement > ) {
|
||||
event.preventDefault();
|
||||
// Notice actions do not support target anchor prop,
|
||||
// so this forces the page to be opened in a new tab.
|
||||
window.open( product.permalink, '_blank' );
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
createSuccessNotice( noticeContent, noticeOptions );
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { PanelBody } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
|
@ -13,22 +12,15 @@ import {
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import useProductEntityProp from '../../../hooks/use-product-entity-prop';
|
||||
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
|
||||
import { isSiteSettingsTime12HourFormatted } from '../../../utils';
|
||||
import { ScheduleSectionProps } from './types';
|
||||
|
||||
export function ScheduleSection( { postType }: ScheduleSectionProps ) {
|
||||
const [ productId ] = useProductEntityProp< number >( 'id' );
|
||||
const { schedule, date, formattedDate } = useProductScheduled( postType );
|
||||
|
||||
// @ts-expect-error There are no types for this.
|
||||
const { editEntityRecord } = useDispatch( 'core' );
|
||||
const { setDate, date, formattedDate } = useProductScheduled( postType );
|
||||
|
||||
async function handlePublishDateTimePickerChange( value: string | null ) {
|
||||
await schedule( ( product ) => {
|
||||
return editEntityRecord( 'postType', postType, productId, product );
|
||||
}, value ?? undefined );
|
||||
await setDate( value ?? undefined );
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,3 +7,4 @@ export { default as __experimentalUseProductEntityProp } from './use-product-ent
|
|||
export { default as __experimentalUseProductMetadata } from './use-product-metadata';
|
||||
export { useProductTemplate as __experimentalUseProductTemplate } from './use-product-template';
|
||||
export { useProductScheduled as __experimentalUseProductScheduled } from './use-product-scheduled';
|
||||
export { useProductManager as __experimentalUseProductManager } from './use-product-manager';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-product-manager';
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import type { Product, ProductStatus } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidations } from '../../contexts/validation-context';
|
||||
import type { WPError } from '../../utils/get-product-error-message';
|
||||
|
||||
function errorHandler( error: WPError, productStatus: ProductStatus ) {
|
||||
if ( error.code ) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if ( 'variations' in error && error.variations ) {
|
||||
return {
|
||||
code: 'variable_product_no_variation_prices',
|
||||
message: error.variations,
|
||||
};
|
||||
}
|
||||
|
||||
const errorMessage = Object.values( error ).find(
|
||||
( value ) => value !== undefined
|
||||
) as string | undefined;
|
||||
|
||||
if ( errorMessage !== undefined ) {
|
||||
return {
|
||||
code: 'product_form_field_error',
|
||||
message: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code:
|
||||
productStatus === 'publish' || productStatus === 'future'
|
||||
? 'product_publish_error'
|
||||
: 'product_create_error',
|
||||
};
|
||||
}
|
||||
|
||||
export function useProductManager< T = Product >( postType: string ) {
|
||||
const [ id ] = useEntityProp< number >( 'postType', postType, 'id' );
|
||||
const [ status ] = useEntityProp< ProductStatus >(
|
||||
'postType',
|
||||
postType,
|
||||
'status'
|
||||
);
|
||||
const [ isSaving, setIsSaving ] = useState( false );
|
||||
const [ isTrashing, setTrashing ] = useState( false );
|
||||
const { isValidating, validate } = useValidations< T >();
|
||||
const { isDirty } = useSelect(
|
||||
( select ) => ( {
|
||||
// @ts-expect-error There are no types for this.
|
||||
isDirty: select( 'core' ).hasEditsForEntityRecord(
|
||||
'postType',
|
||||
postType,
|
||||
id
|
||||
),
|
||||
} ),
|
||||
[ postType, id ]
|
||||
);
|
||||
|
||||
async function save( extraProps: Partial< T > = {} ) {
|
||||
try {
|
||||
setIsSaving( true );
|
||||
|
||||
await validate( extraProps );
|
||||
|
||||
// @ts-expect-error There are no types for this.
|
||||
const { editEntityRecord, saveEditedEntityRecord } =
|
||||
dispatch( 'core' );
|
||||
|
||||
await editEntityRecord< T >( 'postType', postType, id, extraProps );
|
||||
|
||||
const savedProduct = await saveEditedEntityRecord< T >(
|
||||
'postType',
|
||||
postType,
|
||||
id,
|
||||
{
|
||||
throwOnError: true,
|
||||
}
|
||||
);
|
||||
|
||||
return savedProduct as T;
|
||||
} catch ( error ) {
|
||||
throw errorHandler( error as WPError, status );
|
||||
} finally {
|
||||
setIsSaving( false );
|
||||
}
|
||||
}
|
||||
|
||||
async function publish( extraProps: Partial< T > = {} ) {
|
||||
const isPublished = status === 'publish' || status === 'future';
|
||||
|
||||
// The publish button click not only change the status of the product
|
||||
// but also save all the pending changes. So even if the status is
|
||||
// publish it's possible to save the product too.
|
||||
const data: Partial< T > = isPublished
|
||||
? extraProps
|
||||
: { status: 'publish', ...extraProps };
|
||||
|
||||
return save( data );
|
||||
}
|
||||
|
||||
async function trash( force = false ) {
|
||||
try {
|
||||
setTrashing( true );
|
||||
|
||||
await validate();
|
||||
|
||||
// @ts-expect-error There are no types for this.
|
||||
const { deleteEntityRecord, saveEditedEntityRecord } =
|
||||
dispatch( 'core' );
|
||||
|
||||
await saveEditedEntityRecord< T >( 'postType', postType, id, {
|
||||
throwOnError: true,
|
||||
} );
|
||||
|
||||
const deletedProduct = await deleteEntityRecord< T >(
|
||||
'postType',
|
||||
postType,
|
||||
id,
|
||||
{
|
||||
force,
|
||||
throwOnError: true,
|
||||
}
|
||||
);
|
||||
|
||||
return deletedProduct as T;
|
||||
} catch ( error ) {
|
||||
throw errorHandler( error as WPError, status );
|
||||
} finally {
|
||||
setTrashing( false );
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValidating,
|
||||
isDirty,
|
||||
isSaving,
|
||||
isPublishing: isSaving,
|
||||
isTrashing,
|
||||
save,
|
||||
publish,
|
||||
trash,
|
||||
};
|
||||
}
|
|
@ -3,38 +3,33 @@
|
|||
*/
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { getDate, isInTheFuture, date as parseDate } from '@wordpress/date';
|
||||
import { Product, ProductStatus, ProductVariation } from '@woocommerce/data';
|
||||
import type { ProductStatus } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatScheduleDatetime, getSiteDatetime } from '../../utils';
|
||||
import { useProductManager } from '../use-product-manager';
|
||||
|
||||
export const TIMEZONELESS_FORMAT = 'Y-m-d\\TH:i:s';
|
||||
|
||||
export function useProductScheduled( postType: string ) {
|
||||
const [ date ] = useEntityProp< string >(
|
||||
const { isSaving, save } = useProductManager( postType );
|
||||
|
||||
const [ date, set ] = useEntityProp< string >(
|
||||
'postType',
|
||||
postType,
|
||||
'date_created_gmt'
|
||||
);
|
||||
|
||||
const [ editedStatus, , prevStatus ] = useEntityProp< ProductStatus >(
|
||||
'postType',
|
||||
postType,
|
||||
'status'
|
||||
);
|
||||
const [ editedStatus, setStatus, prevStatus ] =
|
||||
useEntityProp< ProductStatus >( 'postType', postType, 'status' );
|
||||
|
||||
const gmtDate = `${ date }+00:00`;
|
||||
|
||||
const siteDate = getSiteDatetime( gmtDate );
|
||||
|
||||
async function schedule(
|
||||
publish: (
|
||||
productOrVariation?: Partial< Product | ProductVariation >
|
||||
) => Promise< Product | ProductVariation | undefined >,
|
||||
value?: string
|
||||
) {
|
||||
function calcDateAndStatus( value?: string ) {
|
||||
const newSiteDate = getDate( value ?? null );
|
||||
const newGmtDate = parseDate( TIMEZONELESS_FORMAT, newSiteDate, 'GMT' );
|
||||
|
||||
|
@ -45,16 +40,28 @@ export function useProductScheduled( postType: string ) {
|
|||
status = 'publish';
|
||||
}
|
||||
|
||||
return publish( {
|
||||
status,
|
||||
date_created_gmt: newGmtDate,
|
||||
} );
|
||||
return { status, date_created_gmt: newGmtDate };
|
||||
}
|
||||
|
||||
async function setDate( value?: string ) {
|
||||
const result = calcDateAndStatus( value );
|
||||
|
||||
set( result.date_created_gmt );
|
||||
setStatus( result.status );
|
||||
}
|
||||
|
||||
async function schedule( value?: string ) {
|
||||
const result = calcDateAndStatus( value );
|
||||
|
||||
return save( result );
|
||||
}
|
||||
|
||||
return {
|
||||
isScheduling: isSaving,
|
||||
isScheduled: editedStatus === 'future' || isInTheFuture( siteDate ),
|
||||
date: siteDate,
|
||||
formattedDate: formatScheduleDatetime( gmtDate ),
|
||||
setDate,
|
||||
schedule,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue