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:
parent
450b06a1e1
commit
d8037dd154
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add duplicate product to store #46294
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
} >;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add menu item to Publish button with copy to draft #46294
|
|
@ -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={ () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue