From d0f095df72c0bf8c89f7c2747c917d7077cc9e1d Mon Sep 17 00:00:00 2001 From: Maikel Perez Date: Mon, 4 Mar 2024 14:04:38 -0300 Subject: [PATCH] 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 --- .../js/product-editor/changelog/add-43047 | 4 + .../button-with-dropdown-menu/index.tsx | 15 +- .../button-with-dropdown-menu/types.ts | 2 + .../header/hooks/use-publish/use-publish.tsx | 118 ++----------- .../publish-button-menu/index.ts | 2 + .../publish-button-menu.tsx | 155 +++++++++++++++++ .../publish-button-menu/types.ts | 8 + .../header/publish-button/publish-button.tsx | 157 +++--------------- .../header/publish-button/utils/index.ts | 1 + .../utils/show-success-notice.ts | 57 +++++++ .../schedule-section/schedule-section.tsx | 12 +- packages/js/product-editor/src/hooks/index.ts | 1 + .../src/hooks/use-product-manager/index.ts | 1 + .../use-product-manager.ts | 152 +++++++++++++++++ .../use-product-scheduled.ts | 41 +++-- 15 files changed, 452 insertions(+), 274 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-43047 create mode 100644 packages/js/product-editor/src/components/header/publish-button/publish-button-menu/index.ts create mode 100644 packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx create mode 100644 packages/js/product-editor/src/components/header/publish-button/publish-button-menu/types.ts create mode 100644 packages/js/product-editor/src/components/header/publish-button/utils/index.ts create mode 100644 packages/js/product-editor/src/components/header/publish-button/utils/show-success-notice.ts create mode 100644 packages/js/product-editor/src/hooks/use-product-manager/index.ts create mode 100644 packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts diff --git a/packages/js/product-editor/changelog/add-43047 b/packages/js/product-editor/changelog/add-43047 new file mode 100644 index 00000000000..c767d09b4e3 --- /dev/null +++ b/packages/js/product-editor/changelog/add-43047 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add menu item to publish button with 'Move to trash' diff --git a/packages/js/product-editor/src/components/button-with-dropdown-menu/index.tsx b/packages/js/product-editor/src/components/button-with-dropdown-menu/index.tsx index e9f93799afb..018dee2b704 100644 --- a/packages/js/product-editor/src/components/button-with-dropdown-menu/index.tsx +++ b/packages/js/product-editor/src/components/button-with-dropdown-menu/index.tsx @@ -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 ( + > + { renderMenu } + ); -}; +} diff --git a/packages/js/product-editor/src/components/button-with-dropdown-menu/types.ts b/packages/js/product-editor/src/components/button-with-dropdown-menu/types.ts index efbc5a2a62a..4cdba2bdefa 100644 --- a/packages/js/product-editor/src/components/button-with-dropdown-menu/types.ts +++ b/packages/js/product-editor/src/components/button-with-dropdown-menu/types.ts @@ -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; }; diff --git a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx index d8c22ee672e..f4805ee9e47 100644 --- a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx +++ b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx @@ -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, }; } diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/index.ts b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/index.ts new file mode 100644 index 00000000000..d1fdefc59ec --- /dev/null +++ b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/index.ts @@ -0,0 +1,2 @@ +export * from './publish-button-menu'; +export * from './types'; diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx new file mode 100644 index 00000000000..4a08bb1ab25 --- /dev/null +++ b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx @@ -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 && ( + setShowScheduleModal( undefined ) } + onSchedule={ scheduleProduct } + /> + ) + ); + } + + function renderMenu( { onClose }: Dropdown.RenderProps ) { + return ( + <> + + { isScheduled ? ( + <> + { + scheduleProduct(); + onClose(); + } } + > + { __( 'Publish now', 'woocommerce' ) } + + { + setShowScheduleModal( 'edit' ); + onClose(); + } } + > + { __( 'Edit schedule', 'woocommerce' ) } + + + ) : ( + { + setShowScheduleModal( 'schedule' ); + onClose(); + } } + > + { __( 'Schedule publish', 'woocommerce' ) } + + ) } + + + { prevStatus !== 'trash' && ( + + { + 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' ) } + + + ) } + + ); + } + + return ( + <> + + + { renderSchedulePublishModal() } + + ); +} diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/types.ts b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/types.ts new file mode 100644 index 00000000000..1acf4529136 --- /dev/null +++ b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/types.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { ButtonWithDropdownMenuProps } from '../../../button-with-dropdown-menu'; + +export type PublishButtonMenuProps = ButtonWithDropdownMenuProps & { + postType: string; +}; diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx b/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx index cb37b2f04f4..106f43e8526 100644 --- a/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx +++ b/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx @@ -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: ( -
-
- { __( - 'Edit schedule', - 'woocommerce' - ) } -
-
{ formattedDate }
-
- ), - onClick() { - setShowScheduleModal( 'edit' ); - }, - }, - ] - : [ - { - title: __( 'Schedule publish', 'woocommerce' ), - onClick() { - setShowScheduleModal( 'schedule' ); - }, - }, - ], - ]; - } - - function renderSchedulePublishModal() { + function renderPublishButtonMenu( + menuProps: Dropdown.RenderProps + ): React.ReactElement { return ( - showScheduleModal && ( - setShowScheduleModal( undefined ) } - onSchedule={ async ( value ) => { - await schedule( publish, value ); - setShowScheduleModal( undefined ); - } } - /> - ) + ); } @@ -198,27 +95,23 @@ export function PublishButton( { } return ( - <> - - - { renderSchedulePublishModal() } - + ); } return ( - <> - - - { renderSchedulePublishModal() } - + ); } diff --git a/packages/js/product-editor/src/components/header/publish-button/utils/index.ts b/packages/js/product-editor/src/components/header/publish-button/utils/index.ts new file mode 100644 index 00000000000..e49c733ead0 --- /dev/null +++ b/packages/js/product-editor/src/components/header/publish-button/utils/index.ts @@ -0,0 +1 @@ +export * from './show-success-notice'; diff --git a/packages/js/product-editor/src/components/header/publish-button/utils/show-success-notice.ts b/packages/js/product-editor/src/components/header/publish-button/utils/show-success-notice.ts new file mode 100644 index 00000000000..a112cf4c39f --- /dev/null +++ b/packages/js/product-editor/src/components/header/publish-button/utils/show-success-notice.ts @@ -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 ); +} diff --git a/packages/js/product-editor/src/components/prepublish-panel/schedule-section/schedule-section.tsx b/packages/js/product-editor/src/components/prepublish-panel/schedule-section/schedule-section.tsx index 67205bd1b9f..4e1861ff2eb 100644 --- a/packages/js/product-editor/src/components/prepublish-panel/schedule-section/schedule-section.tsx +++ b/packages/js/product-editor/src/components/prepublish-panel/schedule-section/schedule-section.tsx @@ -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 ( diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts index 5893d641a58..e7fea059614 100644 --- a/packages/js/product-editor/src/hooks/index.ts +++ b/packages/js/product-editor/src/hooks/index.ts @@ -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'; diff --git a/packages/js/product-editor/src/hooks/use-product-manager/index.ts b/packages/js/product-editor/src/hooks/use-product-manager/index.ts new file mode 100644 index 00000000000..3c28c51297b --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-manager/index.ts @@ -0,0 +1 @@ +export * from './use-product-manager'; diff --git a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts new file mode 100644 index 00000000000..ea392fdcfcf --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts @@ -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, + }; +} diff --git a/packages/js/product-editor/src/hooks/use-product-scheduled/use-product-scheduled.ts b/packages/js/product-editor/src/hooks/use-product-scheduled/use-product-scheduled.ts index 95839e014ca..5f1afcba289 100644 --- a/packages/js/product-editor/src/hooks/use-product-scheduled/use-product-scheduled.ts +++ b/packages/js/product-editor/src/hooks/use-product-scheduled/use-product-scheduled.ts @@ -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, }; }