Add menu item to Publish button with copy to draft (#46294)

* Add duplicate product store

* Add copyToDraft method

* Add copy to draft button

* Modify success notice

* Add changelogs

* Update name before saving product

* Add comment to code
This commit is contained in:
Fernando Marichal 2024-04-12 10:19:12 -03:00 committed by GitHub
parent 450b06a1e1
commit d8037dd154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 143 additions and 3 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add duplicate product to store #46294

View File

@ -14,6 +14,9 @@ export enum TYPES {
DELETE_PRODUCT_START = 'DELETE_PRODUCT_START',
DELETE_PRODUCT_ERROR = 'DELETE_PRODUCT_ERROR',
DELETE_PRODUCT_SUCCESS = 'DELETE_PRODUCT_SUCCESS',
DUPLICATE_PRODUCT_START = 'DUPLICATE_PRODUCT_START',
DUPLICATE_PRODUCT_ERROR = 'DUPLICATE_PRODUCT_ERROR',
DUPLICATE_PRODUCT_SUCCESS = 'DUPLICATE_PRODUCT_SUCCESS',
SET_SUGGESTED_PRODUCTS = 'SET_SUGGESTED_PRODUCTS',
}

View File

@ -57,6 +57,29 @@ export function createProductError(
};
}
function duplicateProductStart( id: number ) {
return {
type: TYPES.DUPLICATE_PRODUCT_START as const,
id,
};
}
function duplicateProductSuccess( id: number, product: Partial< Product > ) {
return {
type: TYPES.DUPLICATE_PRODUCT_SUCCESS as const,
id,
product,
};
}
export function duplicateProductError( id: number, error: unknown ) {
return {
type: TYPES.DUPLICATE_PRODUCT_ERROR as const,
id,
error,
};
}
function updateProductStart( id: number ) {
return {
type: TYPES.UPDATE_PRODUCT_START as const,
@ -165,6 +188,26 @@ export function* updateProduct(
}
}
export function* duplicateProduct(
id: number,
data: Partial< Omit< Product, ReadOnlyProperties > >
): Generator< unknown, Product, Product > {
yield duplicateProductStart( id );
try {
const product: Product = yield apiFetch( {
path: `${ WC_PRODUCT_NAMESPACE }/${ id }/duplicate`,
method: 'POST',
data,
} );
yield duplicateProductSuccess( product.id, product );
return product;
} catch ( error ) {
yield duplicateProductError( id, error );
throw error;
}
}
export function deleteProductStart( id: number ) {
return {
type: TYPES.DELETE_PRODUCT_START as const,
@ -240,6 +283,9 @@ export type Actions = ReturnType<
| typeof deleteProductStart
| typeof deleteProductSuccess
| typeof deleteProductError
| typeof duplicateProductStart
| typeof duplicateProductError
| typeof duplicateProductSuccess
| typeof setSuggestedProductAction
>;
@ -247,4 +293,5 @@ export type ActionDispatchers = DispatchFromMap< {
createProduct: typeof createProduct;
updateProduct: typeof updateProduct;
deleteProduct: typeof deleteProduct;
duplicateProduct: typeof duplicateProduct;
} >;

View File

@ -31,6 +31,7 @@ export type ProductState = {
pending: {
createProduct?: boolean;
updateProduct?: Record< number, boolean >;
duplicateProduct?: Record< number, boolean >;
deleteProduct?: Record< number, boolean >;
};
@ -71,9 +72,20 @@ const reducer: Reducer< ProductState, Actions > = (
},
},
};
case TYPES.DUPLICATE_PRODUCT_START:
return {
...state,
pending: {
duplicateProduct: {
...( state.pending.duplicateProduct || {} ),
[ payload.id ]: true,
},
},
};
case TYPES.CREATE_PRODUCT_SUCCESS:
case TYPES.GET_PRODUCT_SUCCESS:
case TYPES.UPDATE_PRODUCT_SUCCESS:
case TYPES.DUPLICATE_PRODUCT_SUCCESS:
const productData = state.data || {};
return {
...state,
@ -86,6 +98,10 @@ const reducer: Reducer< ProductState, Actions > = (
},
pending: {
createProduct: false,
duplicateProduct: {
...( state.pending.duplicateProduct || {} ),
[ payload.id ]: false,
},
updateProduct: {
...( state.pending.updateProduct || {} ),
[ payload.id ]: false,
@ -158,6 +174,14 @@ const reducer: Reducer< ProductState, Actions > = (
[ `update/${ payload.id }` ]: payload.error,
},
};
case TYPES.DUPLICATE_PRODUCT_ERROR:
return {
...state,
errors: {
...state.errors,
[ `duplicate/${ payload.id }` ]: payload.error,
},
};
case TYPES.DELETE_PRODUCT_START:
return {
...state,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add menu item to Publish button with copy to draft #46294

View File

@ -7,7 +7,7 @@ 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 { getNewPath, navigateTo } from '@woocommerce/navigation';
import { getAdminLink } from '@woocommerce/settings';
/**
@ -31,7 +31,7 @@ export function PublishButtonMenu( {
const [ showScheduleModal, setShowScheduleModal ] = useState<
'schedule' | 'edit' | undefined
>();
const { trash } = useProductManager( postType );
const { copyToDraft, trash } = useProductManager( postType );
const { createErrorNotice, createSuccessNotice } =
useDispatch( 'core/notices' );
const [ , , prevStatus ] = useEntityProp< ProductStatus >(
@ -108,6 +108,36 @@ export function PublishButtonMenu( {
{ prevStatus !== 'trash' && (
<MenuGroup>
<MenuItem
onClick={ () => {
copyToDraft()
.then( ( duplicatedProduct ) => {
recordProductEvent(
'product_copied_to_draft',
duplicatedProduct
);
createSuccessNotice(
__(
'Product successfully duplicated',
'woocommerce'
)
);
const url = getNewPath(
{},
`/product/${ duplicatedProduct.id }`
);
navigateTo( { url } );
} )
.catch( ( error ) => {
const message =
getProductErrorMessage( error );
createErrorNotice( message );
} );
onClose();
} }
>
{ __( 'Copy to a new draft', 'woocommerce' ) }
</MenuItem>
<MenuItem
isDestructive
onClick={ () => {

View File

@ -4,13 +4,14 @@
import { useEntityProp } from '@wordpress/core-data';
import { dispatch, useSelect, select as wpSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import type { Product, ProductStatus } from '@woocommerce/data';
import { Product, ProductStatus, PRODUCTS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { useValidations } from '../../contexts/validation-context';
import type { WPError } from '../../utils/get-product-error-message';
import { AUTO_DRAFT_NAME } from '../../utils/constants';
function errorHandler( error: WPError, productStatus: ProductStatus ) {
if ( error.code ) {
@ -45,6 +46,11 @@ function errorHandler( error: WPError, productStatus: ProductStatus ) {
export function useProductManager< T = Product >( postType: string ) {
const [ id ] = useEntityProp< number >( 'postType', postType, 'id' );
const [ name, , prevName ] = useEntityProp< string >(
'postType',
postType,
'name'
);
const [ status ] = useEntityProp< ProductStatus >(
'postType',
postType,
@ -102,6 +108,27 @@ export function useProductManager< T = Product >( postType: string ) {
}
}
async function copyToDraft() {
try {
// When "Copy to a new draft" is used on an unsaved product with a filled-out name,
// the name is retained in the copied product.
const data =
AUTO_DRAFT_NAME === prevName && name !== prevName
? { name }
: {};
setIsSaving( true );
const duplicatedProduct = await dispatch(
PRODUCTS_STORE_NAME
).duplicateProduct( id, data );
return duplicatedProduct 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';
@ -156,5 +183,6 @@ export function useProductManager< T = Product >( postType: string ) {
save,
publish,
trash,
copyToDraft,
};
}