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 * from './types';
|
||||||
|
|
||||||
export const ButtonWithDropdownMenu: React.FC<
|
export function ButtonWithDropdownMenu( {
|
||||||
ButtonWithDropdownMenuProps
|
|
||||||
> = ( {
|
|
||||||
dropdownButtonLabel = __( 'More options', 'woocommerce' ),
|
dropdownButtonLabel = __( 'More options', 'woocommerce' ),
|
||||||
controls = [],
|
controls,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
popoverProps: {
|
popoverProps: {
|
||||||
placement = 'bottom-end',
|
placement = 'bottom-end',
|
||||||
|
@ -29,8 +27,9 @@ export const ButtonWithDropdownMenu: React.FC<
|
||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
|
renderMenu,
|
||||||
...props
|
...props
|
||||||
} ) => {
|
}: ButtonWithDropdownMenuProps ) {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
className={ `woocommerce-button-with-dropdown-menu${
|
className={ `woocommerce-button-with-dropdown-menu${
|
||||||
|
@ -65,8 +64,10 @@ export const ButtonWithDropdownMenu: React.FC<
|
||||||
offset,
|
offset,
|
||||||
} }
|
} }
|
||||||
defaultOpen={ defaultOpen }
|
defaultOpen={ defaultOpen }
|
||||||
/>
|
>
|
||||||
|
{ renderMenu }
|
||||||
|
</DropdownMenu>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import type {
|
import type {
|
||||||
|
Dropdown,
|
||||||
// @ts-expect-error no exported member.
|
// @ts-expect-error no exported member.
|
||||||
DropdownOption,
|
DropdownOption,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
|
@ -44,4 +45,5 @@ export type ButtonWithDropdownMenuProps = Omit<
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
controls?: DropdownOption[];
|
controls?: DropdownOption[];
|
||||||
popoverProps?: PopoverProps;
|
popoverProps?: PopoverProps;
|
||||||
|
renderMenu?( props: Dropdown.RenderProps ): React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,18 +4,17 @@
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import { useEntityProp } from '@wordpress/core-data';
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import type { Product, ProductVariation } from '@woocommerce/data';
|
import type { Product } from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* 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 { WPError } from '../../../../utils/get-product-error-message';
|
||||||
import type { PublishButtonProps } from '../../publish-button';
|
import type { PublishButtonProps } from '../../publish-button';
|
||||||
|
|
||||||
export function usePublish( {
|
export function usePublish< T = Product >( {
|
||||||
productType = 'product',
|
productType = 'product',
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -23,20 +22,11 @@ export function usePublish( {
|
||||||
onPublishError,
|
onPublishError,
|
||||||
...props
|
...props
|
||||||
}: PublishButtonProps & {
|
}: PublishButtonProps & {
|
||||||
onPublishSuccess?( product: Product ): void;
|
onPublishSuccess?( product: T ): void;
|
||||||
onPublishError?( error: WPError ): void;
|
onPublishError?( error: WPError ): void;
|
||||||
} ): Button.ButtonProps & {
|
} ): Button.ButtonProps {
|
||||||
publish(
|
const { isValidating, isDirty, isPublishing, publish } =
|
||||||
productOrVariation?: Partial< Product | ProductVariation >
|
useProductManager( productType );
|
||||||
): Promise< Product | ProductVariation | undefined >;
|
|
||||||
} {
|
|
||||||
const { isValidating, validate } = useValidations< Product >();
|
|
||||||
|
|
||||||
const [ productId ] = useEntityProp< number >(
|
|
||||||
'postType',
|
|
||||||
productType,
|
|
||||||
'id'
|
|
||||||
);
|
|
||||||
|
|
||||||
const [ status, , prevStatus ] = useEntityProp< Product[ 'status' ] >(
|
const [ status, , prevStatus ] = useEntityProp< Product[ 'status' ] >(
|
||||||
'postType',
|
'postType',
|
||||||
|
@ -44,97 +34,10 @@ export function usePublish( {
|
||||||
'status'
|
'status'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isSaving, isDirty } = useSelect(
|
const isBusy = isPublishing || isValidating;
|
||||||
( 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 isDisabled = disabled || isBusy || ! isDirty;
|
const isDisabled = disabled || isBusy || ! isDirty;
|
||||||
|
|
||||||
// @ts-expect-error There are no types for this.
|
function handleClick( event: MouseEvent< HTMLButtonElement > ) {
|
||||||
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 > ) {
|
|
||||||
if ( isDisabled ) {
|
if ( isDisabled ) {
|
||||||
event.preventDefault?.();
|
event.preventDefault?.();
|
||||||
return;
|
return;
|
||||||
|
@ -144,7 +47,7 @@ export function usePublish( {
|
||||||
onClick( event );
|
onClick( event );
|
||||||
}
|
}
|
||||||
|
|
||||||
await publish();
|
publish().then( onPublishSuccess ).catch( onPublishError );
|
||||||
}
|
}
|
||||||
|
|
||||||
function getButtonText() {
|
function getButtonText() {
|
||||||
|
@ -169,6 +72,5 @@ export function usePublish( {
|
||||||
'aria-disabled': isDisabled,
|
'aria-disabled': isDisabled,
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
onClick: handleClick,
|
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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { MouseEvent, useState } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button, Dropdown } from '@wordpress/components';
|
||||||
import { useEntityProp } from '@wordpress/core-data';
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
import { dispatch, useDispatch } from '@wordpress/data';
|
import { useDispatch } from '@wordpress/data';
|
||||||
import { createElement, Fragment } from '@wordpress/element';
|
import { createElement } from '@wordpress/element';
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { type Product } from '@woocommerce/data';
|
import { type Product } from '@woocommerce/data';
|
||||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
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 { recordProductEvent } from '../../../utils/record-product-event';
|
||||||
import { useFeedbackBar } from '../../../hooks/use-feedback-bar';
|
import { useFeedbackBar } from '../../../hooks/use-feedback-bar';
|
||||||
import { TRACKS_SOURCE } from '../../../constants';
|
import { TRACKS_SOURCE } from '../../../constants';
|
||||||
import { ButtonWithDropdownMenu } from '../../button-with-dropdown-menu';
|
|
||||||
import { usePublish } from '../hooks/use-publish';
|
import { usePublish } from '../hooks/use-publish';
|
||||||
import { PublishButtonProps } from './types';
|
import { PublishButtonMenu } from './publish-button-menu';
|
||||||
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
|
import { showSuccessNotice } from './utils';
|
||||||
import { SchedulePublishModal } from '../../schedule-publish-modal';
|
import type { PublishButtonProps } from './types';
|
||||||
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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PublishButton( {
|
export function PublishButton( {
|
||||||
productType = 'product',
|
productType = 'product',
|
||||||
|
@ -87,7 +38,7 @@ export function PublishButton( {
|
||||||
'status'
|
'status'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { publish, ...publishButtonProps } = usePublish( {
|
const publishButtonProps = usePublish( {
|
||||||
productType,
|
productType,
|
||||||
...props,
|
...props,
|
||||||
onPublishSuccess( savedProduct: Product ) {
|
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 (
|
if (
|
||||||
productType === 'product' &&
|
productType === 'product' &&
|
||||||
window.wcAdminFeatures[ 'product-pre-publish-modal' ] &&
|
window.wcAdminFeatures[ 'product-pre-publish-modal' ] &&
|
||||||
prePublish
|
prePublish
|
||||||
) {
|
) {
|
||||||
function getPublishButtonControls() {
|
function renderPublishButtonMenu(
|
||||||
return [
|
menuProps: Dropdown.RenderProps
|
||||||
isScheduled
|
): React.ReactElement {
|
||||||
? [
|
|
||||||
{
|
|
||||||
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() {
|
|
||||||
return (
|
return (
|
||||||
showScheduleModal && (
|
<PublishButtonMenu { ...menuProps } postType={ productType } />
|
||||||
<SchedulePublishModal
|
|
||||||
postType={ productType }
|
|
||||||
value={
|
|
||||||
showScheduleModal === 'edit' ? date : undefined
|
|
||||||
}
|
|
||||||
onCancel={ () => setShowScheduleModal( undefined ) }
|
|
||||||
onSchedule={ async ( value ) => {
|
|
||||||
await schedule( publish, value );
|
|
||||||
setShowScheduleModal( undefined );
|
|
||||||
} }
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,27 +95,23 @@ export function PublishButton( {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PublishButtonMenu
|
||||||
<ButtonWithDropdownMenu
|
|
||||||
{ ...publishButtonProps }
|
{ ...publishButtonProps }
|
||||||
|
postType={ productType }
|
||||||
|
controls={ undefined }
|
||||||
onClick={ handlePrePublishButtonClick }
|
onClick={ handlePrePublishButtonClick }
|
||||||
controls={ getPublishButtonControls() }
|
renderMenu={ renderPublishButtonMenu }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ renderSchedulePublishModal() }
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PublishButtonMenu
|
||||||
<ButtonWithDropdownMenu
|
|
||||||
{ ...publishButtonProps }
|
{ ...publishButtonProps }
|
||||||
controls={ getPublishButtonControls() }
|
postType={ productType }
|
||||||
|
controls={ undefined }
|
||||||
|
renderMenu={ renderPublishButtonMenu }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ renderSchedulePublishModal() }
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { PanelBody } from '@wordpress/components';
|
import { PanelBody } from '@wordpress/components';
|
||||||
import { useDispatch } from '@wordpress/data';
|
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
|
@ -13,22 +12,15 @@ import {
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import useProductEntityProp from '../../../hooks/use-product-entity-prop';
|
|
||||||
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
|
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
|
||||||
import { isSiteSettingsTime12HourFormatted } from '../../../utils';
|
import { isSiteSettingsTime12HourFormatted } from '../../../utils';
|
||||||
import { ScheduleSectionProps } from './types';
|
import { ScheduleSectionProps } from './types';
|
||||||
|
|
||||||
export function ScheduleSection( { postType }: ScheduleSectionProps ) {
|
export function ScheduleSection( { postType }: ScheduleSectionProps ) {
|
||||||
const [ productId ] = useProductEntityProp< number >( 'id' );
|
const { setDate, date, formattedDate } = useProductScheduled( postType );
|
||||||
const { schedule, date, formattedDate } = useProductScheduled( postType );
|
|
||||||
|
|
||||||
// @ts-expect-error There are no types for this.
|
|
||||||
const { editEntityRecord } = useDispatch( 'core' );
|
|
||||||
|
|
||||||
async function handlePublishDateTimePickerChange( value: string | null ) {
|
async function handlePublishDateTimePickerChange( value: string | null ) {
|
||||||
await schedule( ( product ) => {
|
await setDate( value ?? undefined );
|
||||||
return editEntityRecord( 'postType', postType, productId, product );
|
|
||||||
}, value ?? undefined );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -7,3 +7,4 @@ export { default as __experimentalUseProductEntityProp } from './use-product-ent
|
||||||
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';
|
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 { useEntityProp } from '@wordpress/core-data';
|
||||||
import { getDate, isInTheFuture, date as parseDate } from '@wordpress/date';
|
import { getDate, isInTheFuture, date as parseDate } from '@wordpress/date';
|
||||||
import { Product, ProductStatus, ProductVariation } from '@woocommerce/data';
|
import type { ProductStatus } from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { formatScheduleDatetime, getSiteDatetime } from '../../utils';
|
import { formatScheduleDatetime, getSiteDatetime } from '../../utils';
|
||||||
|
import { useProductManager } from '../use-product-manager';
|
||||||
|
|
||||||
export const TIMEZONELESS_FORMAT = 'Y-m-d\\TH:i:s';
|
export const TIMEZONELESS_FORMAT = 'Y-m-d\\TH:i:s';
|
||||||
|
|
||||||
export function useProductScheduled( postType: string ) {
|
export function useProductScheduled( postType: string ) {
|
||||||
const [ date ] = useEntityProp< string >(
|
const { isSaving, save } = useProductManager( postType );
|
||||||
|
|
||||||
|
const [ date, set ] = useEntityProp< string >(
|
||||||
'postType',
|
'postType',
|
||||||
postType,
|
postType,
|
||||||
'date_created_gmt'
|
'date_created_gmt'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [ editedStatus, , prevStatus ] = useEntityProp< ProductStatus >(
|
const [ editedStatus, setStatus, prevStatus ] =
|
||||||
'postType',
|
useEntityProp< ProductStatus >( 'postType', postType, 'status' );
|
||||||
postType,
|
|
||||||
'status'
|
|
||||||
);
|
|
||||||
|
|
||||||
const gmtDate = `${ date }+00:00`;
|
const gmtDate = `${ date }+00:00`;
|
||||||
|
|
||||||
const siteDate = getSiteDatetime( gmtDate );
|
const siteDate = getSiteDatetime( gmtDate );
|
||||||
|
|
||||||
async function schedule(
|
function calcDateAndStatus( value?: string ) {
|
||||||
publish: (
|
|
||||||
productOrVariation?: Partial< Product | ProductVariation >
|
|
||||||
) => Promise< Product | ProductVariation | undefined >,
|
|
||||||
value?: string
|
|
||||||
) {
|
|
||||||
const newSiteDate = getDate( value ?? null );
|
const newSiteDate = getDate( value ?? null );
|
||||||
const newGmtDate = parseDate( TIMEZONELESS_FORMAT, newSiteDate, 'GMT' );
|
const newGmtDate = parseDate( TIMEZONELESS_FORMAT, newSiteDate, 'GMT' );
|
||||||
|
|
||||||
|
@ -45,16 +40,28 @@ export function useProductScheduled( postType: string ) {
|
||||||
status = 'publish';
|
status = 'publish';
|
||||||
}
|
}
|
||||||
|
|
||||||
return publish( {
|
return { status, date_created_gmt: newGmtDate };
|
||||||
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 {
|
return {
|
||||||
|
isScheduling: isSaving,
|
||||||
isScheduled: editedStatus === 'future' || isInTheFuture( siteDate ),
|
isScheduled: editedStatus === 'future' || isInTheFuture( siteDate ),
|
||||||
date: siteDate,
|
date: siteDate,
|
||||||
formattedDate: formatScheduleDatetime( gmtDate ),
|
formattedDate: formatScheduleDatetime( gmtDate ),
|
||||||
|
setDate,
|
||||||
schedule,
|
schedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue