Add product link field (#34313)

* Add initial product field and modal for editing product link

* Create a product link modal for editing product link

* Only show permalink when product is published

* Add changelogs

* Add changelog for data package

* Change save button to primary

* Fix merge conflcts

* Add getPermalinkParts selector to products store

* Made use of getPermalinkParts to support draft products
This commit is contained in:
louwie17 2022-08-19 12:05:13 -03:00 committed by GitHub
parent c27565f413
commit 78c28ae9f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 458 additions and 41 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Update resetForm arguments, adding changed fields, touched fields and errors.

View File

@ -31,7 +31,12 @@ export type FormContext< Values extends Record< string, any > > = {
help: string | null | undefined; help: string | null | undefined;
}; };
isValidForm: boolean; isValidForm: boolean;
resetForm: ( initialValues: Values ) => void; resetForm: (
initialValues: Values,
changedFields?: { [ P in keyof Values ]?: boolean | undefined },
touchedFields?: { [ P in keyof Values ]?: boolean | undefined },
errors?: { [ P in keyof Values ]?: string }
) => void;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -110,11 +110,16 @@ function FormComponent< Values extends Record< string, any > >(
validate( values ); validate( values );
}, [] ); }, [] );
const resetForm = ( newInitialValues: Values ) => { const resetForm = (
newInitialValues: Values,
newChangedFields = {},
newTouchedFields = {},
newErrors = {}
) => {
setValues( newInitialValues || {} ); setValues( newInitialValues || {} );
setChangedFields( {} ); setChangedFields( newChangedFields );
setTouched( {} ); setTouched( newTouchedFields );
setErrors( {} ); setErrors( newErrors );
}; };
useImperativeHandle( ref, () => ( { useImperativeHandle( ref, () => ( {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Update types for update and create product.

View File

@ -127,7 +127,7 @@ export function getProductsTotalCountError(
} }
export function* createProduct( export function* createProduct(
data: Omit< Product, ReadOnlyProperties > data: Partial< Omit< Product, ReadOnlyProperties > >
): Generator< unknown, Product, Product > { ): Generator< unknown, Product, Product > {
yield createProductStart(); yield createProductStart();
try { try {
@ -147,7 +147,7 @@ export function* createProduct(
export function* updateProduct( export function* updateProduct(
id: number, id: number,
data: Omit< Product, ReadOnlyProperties > data: Partial< Omit< Product, ReadOnlyProperties > >
): Generator< unknown, Product, Product > { ): Generator< unknown, Product, Product > {
yield updateProductStart( id ); yield updateProductStart( id );
try { try {

View File

@ -1,3 +1,4 @@
export const STORE_NAME = 'wc/admin/products'; export const STORE_NAME = 'wc/admin/products';
export const WC_PRODUCT_NAMESPACE = '/wc/v3/products'; export const WC_PRODUCT_NAMESPACE = '/wc/v3/products';
export const PERMALINK_PRODUCT_REGEX = /%(?:postname|pagename)%/;

View File

@ -19,6 +19,7 @@ registerStore< State >( STORE_NAME, {
reducer: reducer as Reducer< ProductState >, reducer: reducer as Reducer< ProductState >,
actions, actions,
controls, controls,
// @ts-expect-error as the registerStore type is not allowing the createRegistrySelector selector.
selectors, selectors,
resolvers, resolvers,
} ); } );

View File

@ -2,6 +2,7 @@
* External dependencies * External dependencies
*/ */
import createSelector from 'rememo'; import createSelector from 'rememo';
import { createRegistrySelector } from '@wordpress/data';
/** /**
* Internal dependencies * Internal dependencies
@ -14,6 +15,7 @@ import { WPDataSelector, WPDataSelectors } from '../types';
import { ProductState } from './reducer'; import { ProductState } from './reducer';
import { PartialProduct, ProductQuery } from './types'; import { PartialProduct, ProductQuery } from './types';
import { ActionDispatchers } from './actions'; import { ActionDispatchers } from './actions';
import { PERMALINK_PRODUCT_REGEX } from './constants';
export const getProduct = ( export const getProduct = (
state: ProductState, state: ProductState,
@ -120,6 +122,39 @@ export const isPending = (
return false; return false;
}; };
export const getPermalinkParts = createRegistrySelector(
( select ) => ( state: ProductState, productId: number ) => {
const product = select( 'core' ).getEntityRecord(
'postType',
'product',
productId,
// @ts-expect-error query object is not part of the @wordpress/core-data types yet.
{
_fields: [
'id',
'permalink_template',
'slug',
'generated_slug',
],
}
);
if ( product && product.permalink_template ) {
const postName = product.slug || product.generated_slug;
const [ prefix, suffix ] = product.permalink_template.split(
PERMALINK_PRODUCT_REGEX
);
return {
prefix,
postName,
suffix,
};
}
return null;
}
);
export type ProductsSelectors = { export type ProductsSelectors = {
getCreateProductError: WPDataSelector< typeof getCreateProductError >; getCreateProductError: WPDataSelector< typeof getCreateProductError >;
getProduct: WPDataSelector< typeof getProduct >; getProduct: WPDataSelector< typeof getProduct >;
@ -127,4 +162,7 @@ export type ProductsSelectors = {
getProductsTotalCount: WPDataSelector< typeof getProductsTotalCount >; getProductsTotalCount: WPDataSelector< typeof getProductsTotalCount >;
getProductsError: WPDataSelector< typeof getProductsError >; getProductsError: WPDataSelector< typeof getProductsError >;
isPending: WPDataSelector< typeof isPending >; isPending: WPDataSelector< typeof isPending >;
getPermalinkParts: (
productId: number
) => { prefix: string; postName: string; suffix: string } | null;
} & WPDataSelectors; } & WPDataSelectors;

View File

@ -12,7 +12,6 @@ import { Product } from '@woocommerce/data';
import { ProductFormLayout } from './layout/product-form-layout'; import { ProductFormLayout } from './layout/product-form-layout';
import { ProductFormActions } from './product-form-actions'; import { ProductFormActions } from './product-form-actions';
import { ProductDetailsSection } from './sections/product-details-section'; import { ProductDetailsSection } from './sections/product-details-section';
import { ProductImagesSection } from './sections/product-images-section';
import './product-page.scss'; import './product-page.scss';
const AddProductPage: React.FC = () => { const AddProductPage: React.FC = () => {

View File

@ -28,14 +28,32 @@ const EditProductPage: React.FC = () => {
const formRef = useRef< FormRef< Partial< Product > > >( null ); const formRef = useRef< FormRef< Partial< Product > > >( null );
const { product, isLoading, isPendingAction } = useSelect( const { product, isLoading, isPendingAction } = useSelect(
( select: WCDataSelector ) => { ( select: WCDataSelector ) => {
const { getProduct, hasFinishedResolution, isPending } = const {
select( PRODUCTS_STORE_NAME ); getProduct,
hasFinishedResolution,
isPending,
getPermalinkParts,
} = select( PRODUCTS_STORE_NAME );
if ( productId ) { if ( productId ) {
const retrievedProduct = getProduct(
parseInt( productId, 10 ),
undefined
);
const permalinkParts = getPermalinkParts(
parseInt( productId, 10 )
);
return { return {
product: getProduct( parseInt( productId, 10 ), undefined ), product:
isLoading: ! hasFinishedResolution( 'getProduct', [ permalinkParts && retrievedProduct
parseInt( productId, 10 ), ? retrievedProduct
] ), : undefined,
isLoading:
! hasFinishedResolution( 'getProduct', [
parseInt( productId, 10 ),
] ) ||
! hasFinishedResolution( 'getPermalinkParts', [
parseInt( productId, 10 ),
] ),
isPendingAction: isPendingAction:
isPending( 'createProduct' ) || isPending( 'createProduct' ) ||
isPending( isPending(

View File

@ -8,7 +8,6 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
padding: $gap-large; padding: $gap-large;
gap: $gap-smaller;
background: $white; background: $white;
border: 1px solid $gray-400; border: 1px solid $gray-400;
@ -18,4 +17,8 @@
width: 100%; width: 100%;
} }
} }
.product-field-layout:not(:first-child) {
margin-top: $gap;
}
} }

View File

@ -29,7 +29,7 @@ export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
</div> </div>
<div className="product-category-layout__fields"> <div className="product-category-layout__fields">
{ Children.map( children, ( child ) => { { Children.map( children, ( child ) => {
if ( isValidElement( child ) && child.props.name ) { if ( isValidElement( child ) && child.props.onChange ) {
return ( return (
<ProductFieldLayout <ProductFieldLayout
fieldName={ child.props.name } fieldName={ child.props.name }

View File

@ -26,7 +26,9 @@ jest.mock( '../product-field-layout', () => {
}; };
} ); } );
const SampleInputField: React.FC< { name: string } > = ( { name } ) => { const SampleInputField: React.FC< { name: string; onChange: () => void } > = ( {
name,
} ) => {
return <div>smaple-input-field-{ name }</div>; return <div>smaple-input-field-{ name }</div>;
}; };
@ -46,14 +48,14 @@ describe( 'ProductSectionLayout', () => {
expect( queryByText( 'This is a description' ) ).toBeInTheDocument(); expect( queryByText( 'This is a description' ) ).toBeInTheDocument();
} ); } );
it( 'should wrap children in ProductFieldLayout if prop contains name', () => { it( 'should wrap children in ProductFieldLayout if prop contains onChange', () => {
const { queryByText, queryAllByText } = render( const { queryByText, queryAllByText } = render(
<ProductSectionLayout <ProductSectionLayout
title="Title" title="Title"
description="This is a description" description="This is a description"
> >
<SampleInputField name="name" /> <SampleInputField name="name" onChange={ () => {} } />
<SampleInputField name="description" /> <SampleInputField name="description" onChange={ () => {} } />
</ProductSectionLayout> </ProductSectionLayout>
); );
@ -66,7 +68,7 @@ describe( 'ProductSectionLayout', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
} ); } );
it( 'should not wrap children in ProductFieldLayout if prop does not contain name', () => { it( 'should not wrap children in ProductFieldLayout if prop does not contain onChange', () => {
const { queryByText, queryAllByText } = render( const { queryByText, queryAllByText } = render(
<ProductSectionLayout <ProductSectionLayout
title="Title" title="Title"

View File

@ -50,7 +50,11 @@ export const ProductFormActions: React.FC = () => {
if ( ! values.id ) { if ( ! values.id ) {
createProductWithStatus( values, 'draft' ); createProductWithStatus( values, 'draft' );
} else { } else {
const product = await updateProductWithStatus( values, 'draft' ); const product = await updateProductWithStatus(
values.id,
values,
'draft'
);
if ( product && product.id ) { if ( product && product.id ) {
resetForm( product ); resetForm( product );
} }
@ -65,7 +69,11 @@ export const ProductFormActions: React.FC = () => {
if ( ! values.id ) { if ( ! values.id ) {
createProductWithStatus( values, 'publish' ); createProductWithStatus( values, 'publish' );
} else { } else {
const product = await updateProductWithStatus( values, 'publish' ); const product = await updateProductWithStatus(
values.id,
values,
'publish'
);
if ( product && product.id ) { if ( product && product.id ) {
resetForm( product ); resetForm( product );
} }
@ -78,7 +86,7 @@ export const ProductFormActions: React.FC = () => {
...getProductDataForTracks(), ...getProductDataForTracks(),
} ); } );
if ( values.id ) { if ( values.id ) {
await updateProductWithStatus( values, 'publish' ); await updateProductWithStatus( values.id, values, 'publish' );
} else { } else {
await createProductWithStatus( values, 'publish', false, true ); await createProductWithStatus( values, 'publish', false, true );
} }
@ -91,7 +99,11 @@ export const ProductFormActions: React.FC = () => {
...getProductDataForTracks(), ...getProductDataForTracks(),
} ); } );
if ( values.id ) { if ( values.id ) {
await updateProductWithStatus( values, values.status || 'draft' ); await updateProductWithStatus(
values.id,
values,
values.status || 'draft'
);
} }
await copyProductWithStatus( values ); await copyProductWithStatus( values );
}; };

View File

@ -0,0 +1,18 @@
.product-details-section {
&__product-link {
color: $gray-700;
font-size: 12px;
> a {
color: inherit;
text-decoration: none;
font-weight: 600;
}
.components-button.is-link {
font-size: 12px;
text-decoration: none;
margin-left: $gap-smaller;
}
}
}

View File

@ -1,22 +1,47 @@
/** /**
* External dependencies * External dependencies
*/ */
import { CheckboxControl, TextControl } from '@wordpress/components'; import { CheckboxControl, Button, TextControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { cleanForSlug } from '@wordpress/url';
import { EnrichedLabel, useFormContext } from '@woocommerce/components'; import { EnrichedLabel, useFormContext } from '@woocommerce/components';
import { Product } from '@woocommerce/data'; import {
Product,
PRODUCTS_STORE_NAME,
WCDataSelector,
} from '@woocommerce/data';
import classnames from 'classnames'; import classnames from 'classnames';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './product-details-section.scss';
import { ProductSectionLayout } from '../layout/product-section-layout'; import { ProductSectionLayout } from '../layout/product-section-layout';
import { EditProductLinkModal } from '../shared/edit-product-link-modal';
const PRODUCT_DETAILS_SLUG = 'product-details'; const PRODUCT_DETAILS_SLUG = 'product-details';
export const ProductDetailsSection: React.FC = () => { export const ProductDetailsSection: React.FC = () => {
const { getInputProps } = useFormContext< Product >(); const { getInputProps, values } = useFormContext< Product >();
const [ showProductLinkEditModal, setShowProductLinkEditModal ] =
useState( false );
const { permalinkPrefix, permalinkSuffix } = useSelect(
( select: WCDataSelector ) => {
const { getPermalinkParts } = select( PRODUCTS_STORE_NAME );
if ( values.id ) {
const parts = getPermalinkParts( values.id );
return {
permalinkPrefix: parts?.prefix,
permalinkSuffix: parts?.suffix,
};
}
return {};
}
);
const getCheckboxProps = ( item: string ) => { const getCheckboxProps = ( item: string ) => {
const { checked, className, onChange, onBlur } = const { checked, className, onChange, onBlur } =
getInputProps< boolean >( item ); getInputProps< boolean >( item );
@ -67,6 +92,26 @@ export const ProductDetailsSection: React.FC = () => {
placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) } placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) }
{ ...getTextControlProps( 'name' ) } { ...getTextControlProps( 'name' ) }
/> />
{ values.id && permalinkPrefix && (
<div className="product-details-section__product-link">
{ __( 'Product link', 'woocommerce' ) }:&nbsp;
<a
href={ values.permalink }
target="_blank"
rel="noreferrer"
>
{ permalinkPrefix }
{ values.slug || cleanForSlug( values.name ) }
{ permalinkSuffix }
</a>
<Button
variant="link"
onClick={ () => setShowProductLinkEditModal( true ) }
>
{ __( 'Edit', 'woocommerce' ) }
</Button>
</div>
) }
<CheckboxControl <CheckboxControl
label={ label={
<EnrichedLabel <EnrichedLabel
@ -85,6 +130,15 @@ export const ProductDetailsSection: React.FC = () => {
} }
{ ...getCheckboxProps( 'featured' ) } { ...getCheckboxProps( 'featured' ) }
/> />
{ showProductLinkEditModal && (
<EditProductLinkModal
permalinkPrefix={ permalinkPrefix || '' }
permalinkSuffix={ permalinkSuffix || '' }
product={ values }
onCancel={ () => setShowProductLinkEditModal( false ) }
onSaved={ () => setShowProductLinkEditModal( false ) }
/>
) }
</ProductSectionLayout> </ProductSectionLayout>
); );
}; };

View File

@ -0,0 +1,11 @@
.woocommerce-product-link-edit-modal {
min-width: 650px;
&__buttons {
margin-top: $gap-larger;
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;
}
}

View File

@ -0,0 +1,134 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Modal, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { cleanForSlug } from '@wordpress/url';
import { Product } from '@woocommerce/data';
import { Text } from '@woocommerce/experimental';
import { useFormContext } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './edit-product-link-modal.scss';
import { useProductHelper } from '../../use-product-helper';
type EditProductLinkModalProps = {
product: Product;
permalinkPrefix: string;
permalinkSuffix: string;
onCancel: () => void;
onSaved: () => void;
};
export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {
product,
permalinkPrefix,
permalinkSuffix,
onCancel,
onSaved,
} ) => {
const { createNotice } = useDispatch( 'core/notices' );
const { updateProductWithStatus, isUpdatingDraft, isUpdatingPublished } =
useProductHelper();
const [ slug, setSlug ] = useState(
product.slug || cleanForSlug( product.name )
);
const { resetForm, changedFields, touched, errors } =
useFormContext< Product >();
const onSave = async () => {
recordEvent( 'product_update_slug', {
new_product_page: true,
product_id: product.id,
product_type: product.type,
} );
const updatedProduct = await updateProductWithStatus(
product.id,
{
slug,
},
product.status,
true
);
if ( updatedProduct && updatedProduct.id ) {
// only reset the updated slug and permalink fields.
resetForm(
{
...product,
slug: updatedProduct.slug,
permalink: updatedProduct.permalink,
},
changedFields,
touched,
errors
);
createNotice(
updatedProduct.slug === cleanForSlug( slug )
? 'success'
: 'info',
updatedProduct.slug === cleanForSlug( slug )
? __( 'Product link successfully updated.', 'woocommerce' )
: __(
'Product link already existed, updated to ',
'woocommerce'
) + updatedProduct.permalink
);
} else {
createNotice(
'error',
__( 'Failed to update product link.', 'woocommerce' )
);
}
onSaved();
};
const newProductLinkLabel =
permalinkPrefix + cleanForSlug( slug ) + permalinkSuffix;
return (
<Modal
title={ __( 'Edit product link', 'woocommerce' ) }
onRequestClose={ () => onCancel() }
className="woocommerce-product-link-edit-modal"
>
<div className="woocommerce-product-link-edit-modal__wrapper">
<TextControl
label={ newProductLinkLabel }
name="slug"
value={ slug }
onChange={ setSlug }
/>
<Text size={ 12 }>
{ __(
"Use simple, descriptive words and numbers. We'll replace spaces with hyphens (-).",
'woocommerce'
) }
</Text>
<div className="woocommerce-product-link-edit-modal__buttons">
<Button isSecondary onClick={ () => onCancel() }>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
isPrimary
isBusy={ isUpdatingDraft || isUpdatingPublished }
disabled={
isUpdatingDraft ||
isUpdatingPublished ||
slug === product.slug
}
onClick={ () => {
onSave();
} }
>
{ __( 'Save', 'woocommerce' ) }
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1 @@
export * from './edit-product-link-modal';

View File

@ -0,0 +1,87 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { Product } from '@woocommerce/data';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { EditProductLinkModal } from '../';
describe( 'EditProductLinkModal', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
it( 'should show a field with the permalink as label', () => {
const { queryByText } = render(
<EditProductLinkModal
permalinkPrefix={ 'wootesting.com/product/' }
permalinkSuffix={ '' }
product={
{
slug: 'test',
permalink: 'wootesting.com/product/test',
} as Product
}
onCancel={ () => {} }
onSaved={ () => {} }
/>
);
expect(
queryByText( 'wootesting.com/product/test' )
).toBeInTheDocument();
} );
it( 'should update the permalink label as the slug is being updated', () => {
const { queryByText, getByLabelText } = render(
<EditProductLinkModal
permalinkPrefix={ 'wootesting.com/product/' }
permalinkSuffix={ '' }
product={
{
slug: 'test',
permalink: 'wootesting.com/product/test',
} as Product
}
onCancel={ () => {} }
onSaved={ () => {} }
/>
);
userEvent.type(
getByLabelText( 'wootesting.com/product/test' ),
'{esc}{space}update',
{}
);
expect(
queryByText( 'wootesting.com/product/test-update' )
).toBeInTheDocument();
} );
it( 'should only update the end of the permalink incase the slug matches other parts of the url', () => {
const { queryByText, getByLabelText } = render(
<EditProductLinkModal
permalinkPrefix={ 'wootesting.com/product/' }
permalinkSuffix={ '' }
product={
{
slug: 'product',
permalink: 'wootesting.com/product/product',
} as Product
}
onCancel={ () => {} }
onSaved={ () => {} }
/>
);
userEvent.type(
getByLabelText( 'wootesting.com/product/product' ),
'{esc}{space}update',
{}
);
expect(
queryByText( 'wootesting.com/product/product-update' )
).toBeInTheDocument();
} );
} );

View File

@ -177,6 +177,7 @@ describe( 'ProductFormActions', () => {
); );
queryByText( 'Save draft' )?.click(); queryByText( 'Save draft' )?.click();
expect( updateProductWithStatus ).toHaveBeenCalledWith( expect( updateProductWithStatus ).toHaveBeenCalledWith(
product.id,
{ ...product, name: 'Name Update' }, { ...product, name: 'Name Update' },
'draft' 'draft'
); );
@ -219,6 +220,7 @@ describe( 'ProductFormActions', () => {
manage_stock: true, manage_stock: true,
} ); } );
expect( updateProductWithStatus ).toHaveBeenCalledWith( expect( updateProductWithStatus ).toHaveBeenCalledWith(
product.id,
product, product,
'publish' 'publish'
); );
@ -367,6 +369,7 @@ describe( 'ProductFormActions', () => {
); );
updateProductWithStatus.mockReturnValue( Promise.resolve() ); updateProductWithStatus.mockReturnValue( Promise.resolve() );
expect( updateProductWithStatus ).toHaveBeenCalledWith( expect( updateProductWithStatus ).toHaveBeenCalledWith(
product.id,
product, product,
'publish' 'publish'
); );
@ -404,6 +407,7 @@ describe( 'ProductFormActions', () => {
} ); } );
updateProductWithStatus.mockReturnValue( Promise.resolve() ); updateProductWithStatus.mockReturnValue( Promise.resolve() );
expect( updateProductWithStatus ).toHaveBeenCalledWith( expect( updateProductWithStatus ).toHaveBeenCalledWith(
product.id,
product, product,
'publish' 'publish'
); );

View File

@ -107,12 +107,20 @@ export function useProductHelper() {
} }
}, },
() => { () => {
createNotice( if ( ! skipNotice ) {
'error', createNotice(
status === 'publish' 'error',
? __( 'Failed to publish product.', 'woocommerce' ) status === 'publish'
: __( 'Failed to create product.', 'woocommerce' ) ? __(
); 'Failed to publish product.',
'woocommerce'
)
: __(
'Failed to create product.',
'woocommerce'
)
);
}
setUpdating( { setUpdating( {
...updating, ...updating,
[ status ]: false, [ status ]: false,
@ -126,14 +134,16 @@ export function useProductHelper() {
/** /**
* Update product with status. * Update product with status.
* *
* @param {Product} product the product to be updated (should contain product id). * @param {number} productId the product id to be updated.
* @param {Product} product the product to be updated.
* @param {string} status the product status. * @param {string} status the product status.
* @param {boolean} skipNotice if the notice should be skipped (default: false). * @param {boolean} skipNotice if the notice should be skipped (default: false).
* @return {Promise<Product>} Returns a promise with the updated product. * @return {Promise<Product>} Returns a promise with the updated product.
*/ */
const updateProductWithStatus = useCallback( const updateProductWithStatus = useCallback(
async ( async (
product: Product, productId: number,
product: Partial< Product >,
status: ProductStatus, status: ProductStatus,
skipNotice = false skipNotice = false
): Promise< Product > => { ): Promise< Product > => {
@ -141,7 +151,7 @@ export function useProductHelper() {
...updating, ...updating,
[ status ]: true, [ status ]: true,
} ); } );
return updateProduct( product.id, { return updateProduct( productId, {
...product, ...product,
status, status,
} ).then( } ).then(
@ -174,10 +184,12 @@ export function useProductHelper() {
return updatedProduct; return updatedProduct;
}, },
( error ) => { ( error ) => {
createNotice( if ( ! skipNotice ) {
'error', createNotice(
__( 'Failed to update product.', 'woocommerce' ) 'error',
); __( 'Failed to update product.', 'woocommerce' )
);
}
setUpdating( { setUpdating( {
...updating, ...updating,
[ status ]: false, [ status ]: false,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product link field to the new edit product form.