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:
parent
c27565f413
commit
78c28ae9f3
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Update resetForm arguments, adding changed fields, touched fields and errors.
|
|
@ -31,7 +31,12 @@ export type FormContext< Values extends Record< string, any > > = {
|
|||
help: string | null | undefined;
|
||||
};
|
||||
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
|
||||
|
|
|
@ -110,11 +110,16 @@ function FormComponent< Values extends Record< string, any > >(
|
|||
validate( values );
|
||||
}, [] );
|
||||
|
||||
const resetForm = ( newInitialValues: Values ) => {
|
||||
const resetForm = (
|
||||
newInitialValues: Values,
|
||||
newChangedFields = {},
|
||||
newTouchedFields = {},
|
||||
newErrors = {}
|
||||
) => {
|
||||
setValues( newInitialValues || {} );
|
||||
setChangedFields( {} );
|
||||
setTouched( {} );
|
||||
setErrors( {} );
|
||||
setChangedFields( newChangedFields );
|
||||
setTouched( newTouchedFields );
|
||||
setErrors( newErrors );
|
||||
};
|
||||
|
||||
useImperativeHandle( ref, () => ( {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Update types for update and create product.
|
|
@ -127,7 +127,7 @@ export function getProductsTotalCountError(
|
|||
}
|
||||
|
||||
export function* createProduct(
|
||||
data: Omit< Product, ReadOnlyProperties >
|
||||
data: Partial< Omit< Product, ReadOnlyProperties > >
|
||||
): Generator< unknown, Product, Product > {
|
||||
yield createProductStart();
|
||||
try {
|
||||
|
@ -147,7 +147,7 @@ export function* createProduct(
|
|||
|
||||
export function* updateProduct(
|
||||
id: number,
|
||||
data: Omit< Product, ReadOnlyProperties >
|
||||
data: Partial< Omit< Product, ReadOnlyProperties > >
|
||||
): Generator< unknown, Product, Product > {
|
||||
yield updateProductStart( id );
|
||||
try {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const STORE_NAME = 'wc/admin/products';
|
||||
|
||||
export const WC_PRODUCT_NAMESPACE = '/wc/v3/products';
|
||||
export const PERMALINK_PRODUCT_REGEX = /%(?:postname|pagename)%/;
|
||||
|
|
|
@ -19,6 +19,7 @@ registerStore< State >( STORE_NAME, {
|
|||
reducer: reducer as Reducer< ProductState >,
|
||||
actions,
|
||||
controls,
|
||||
// @ts-expect-error as the registerStore type is not allowing the createRegistrySelector selector.
|
||||
selectors,
|
||||
resolvers,
|
||||
} );
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import createSelector from 'rememo';
|
||||
import { createRegistrySelector } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -14,6 +15,7 @@ import { WPDataSelector, WPDataSelectors } from '../types';
|
|||
import { ProductState } from './reducer';
|
||||
import { PartialProduct, ProductQuery } from './types';
|
||||
import { ActionDispatchers } from './actions';
|
||||
import { PERMALINK_PRODUCT_REGEX } from './constants';
|
||||
|
||||
export const getProduct = (
|
||||
state: ProductState,
|
||||
|
@ -120,6 +122,39 @@ export const isPending = (
|
|||
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 = {
|
||||
getCreateProductError: WPDataSelector< typeof getCreateProductError >;
|
||||
getProduct: WPDataSelector< typeof getProduct >;
|
||||
|
@ -127,4 +162,7 @@ export type ProductsSelectors = {
|
|||
getProductsTotalCount: WPDataSelector< typeof getProductsTotalCount >;
|
||||
getProductsError: WPDataSelector< typeof getProductsError >;
|
||||
isPending: WPDataSelector< typeof isPending >;
|
||||
getPermalinkParts: (
|
||||
productId: number
|
||||
) => { prefix: string; postName: string; suffix: string } | null;
|
||||
} & WPDataSelectors;
|
||||
|
|
|
@ -12,7 +12,6 @@ import { Product } from '@woocommerce/data';
|
|||
import { ProductFormLayout } from './layout/product-form-layout';
|
||||
import { ProductFormActions } from './product-form-actions';
|
||||
import { ProductDetailsSection } from './sections/product-details-section';
|
||||
import { ProductImagesSection } from './sections/product-images-section';
|
||||
import './product-page.scss';
|
||||
|
||||
const AddProductPage: React.FC = () => {
|
||||
|
|
|
@ -28,12 +28,30 @@ const EditProductPage: React.FC = () => {
|
|||
const formRef = useRef< FormRef< Partial< Product > > >( null );
|
||||
const { product, isLoading, isPendingAction } = useSelect(
|
||||
( select: WCDataSelector ) => {
|
||||
const { getProduct, hasFinishedResolution, isPending } =
|
||||
select( PRODUCTS_STORE_NAME );
|
||||
const {
|
||||
getProduct,
|
||||
hasFinishedResolution,
|
||||
isPending,
|
||||
getPermalinkParts,
|
||||
} = select( PRODUCTS_STORE_NAME );
|
||||
if ( productId ) {
|
||||
const retrievedProduct = getProduct(
|
||||
parseInt( productId, 10 ),
|
||||
undefined
|
||||
);
|
||||
const permalinkParts = getPermalinkParts(
|
||||
parseInt( productId, 10 )
|
||||
);
|
||||
return {
|
||||
product: getProduct( parseInt( productId, 10 ), undefined ),
|
||||
isLoading: ! hasFinishedResolution( 'getProduct', [
|
||||
product:
|
||||
permalinkParts && retrievedProduct
|
||||
? retrievedProduct
|
||||
: undefined,
|
||||
isLoading:
|
||||
! hasFinishedResolution( 'getProduct', [
|
||||
parseInt( productId, 10 ),
|
||||
] ) ||
|
||||
! hasFinishedResolution( 'getPermalinkParts', [
|
||||
parseInt( productId, 10 ),
|
||||
] ),
|
||||
isPendingAction:
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: $gap-large;
|
||||
gap: $gap-smaller;
|
||||
|
||||
background: $white;
|
||||
border: 1px solid $gray-400;
|
||||
|
@ -18,4 +17,8 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.product-field-layout:not(:first-child) {
|
||||
margin-top: $gap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
|
|||
</div>
|
||||
<div className="product-category-layout__fields">
|
||||
{ Children.map( children, ( child ) => {
|
||||
if ( isValidElement( child ) && child.props.name ) {
|
||||
if ( isValidElement( child ) && child.props.onChange ) {
|
||||
return (
|
||||
<ProductFieldLayout
|
||||
fieldName={ child.props.name }
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -46,14 +48,14 @@ describe( 'ProductSectionLayout', () => {
|
|||
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(
|
||||
<ProductSectionLayout
|
||||
title="Title"
|
||||
description="This is a description"
|
||||
>
|
||||
<SampleInputField name="name" />
|
||||
<SampleInputField name="description" />
|
||||
<SampleInputField name="name" onChange={ () => {} } />
|
||||
<SampleInputField name="description" onChange={ () => {} } />
|
||||
</ProductSectionLayout>
|
||||
);
|
||||
|
||||
|
@ -66,7 +68,7 @@ describe( 'ProductSectionLayout', () => {
|
|||
).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(
|
||||
<ProductSectionLayout
|
||||
title="Title"
|
||||
|
|
|
@ -50,7 +50,11 @@ export const ProductFormActions: React.FC = () => {
|
|||
if ( ! values.id ) {
|
||||
createProductWithStatus( values, 'draft' );
|
||||
} else {
|
||||
const product = await updateProductWithStatus( values, 'draft' );
|
||||
const product = await updateProductWithStatus(
|
||||
values.id,
|
||||
values,
|
||||
'draft'
|
||||
);
|
||||
if ( product && product.id ) {
|
||||
resetForm( product );
|
||||
}
|
||||
|
@ -65,7 +69,11 @@ export const ProductFormActions: React.FC = () => {
|
|||
if ( ! values.id ) {
|
||||
createProductWithStatus( values, 'publish' );
|
||||
} else {
|
||||
const product = await updateProductWithStatus( values, 'publish' );
|
||||
const product = await updateProductWithStatus(
|
||||
values.id,
|
||||
values,
|
||||
'publish'
|
||||
);
|
||||
if ( product && product.id ) {
|
||||
resetForm( product );
|
||||
}
|
||||
|
@ -78,7 +86,7 @@ export const ProductFormActions: React.FC = () => {
|
|||
...getProductDataForTracks(),
|
||||
} );
|
||||
if ( values.id ) {
|
||||
await updateProductWithStatus( values, 'publish' );
|
||||
await updateProductWithStatus( values.id, values, 'publish' );
|
||||
} else {
|
||||
await createProductWithStatus( values, 'publish', false, true );
|
||||
}
|
||||
|
@ -91,7 +99,11 @@ export const ProductFormActions: React.FC = () => {
|
|||
...getProductDataForTracks(),
|
||||
} );
|
||||
if ( values.id ) {
|
||||
await updateProductWithStatus( values, values.status || 'draft' );
|
||||
await updateProductWithStatus(
|
||||
values.id,
|
||||
values,
|
||||
values.status || 'draft'
|
||||
);
|
||||
}
|
||||
await copyProductWithStatus( values );
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,47 @@
|
|||
/**
|
||||
* 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 { useState } from '@wordpress/element';
|
||||
import { cleanForSlug } from '@wordpress/url';
|
||||
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 { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './product-details-section.scss';
|
||||
import { ProductSectionLayout } from '../layout/product-section-layout';
|
||||
import { EditProductLinkModal } from '../shared/edit-product-link-modal';
|
||||
|
||||
const PRODUCT_DETAILS_SLUG = 'product-details';
|
||||
|
||||
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 { checked, className, onChange, onBlur } =
|
||||
getInputProps< boolean >( item );
|
||||
|
@ -67,6 +92,26 @@ export const ProductDetailsSection: React.FC = () => {
|
|||
placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) }
|
||||
{ ...getTextControlProps( 'name' ) }
|
||||
/>
|
||||
{ values.id && permalinkPrefix && (
|
||||
<div className="product-details-section__product-link">
|
||||
{ __( 'Product link', 'woocommerce' ) }:
|
||||
<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
|
||||
label={
|
||||
<EnrichedLabel
|
||||
|
@ -85,6 +130,15 @@ export const ProductDetailsSection: React.FC = () => {
|
|||
}
|
||||
{ ...getCheckboxProps( 'featured' ) }
|
||||
/>
|
||||
{ showProductLinkEditModal && (
|
||||
<EditProductLinkModal
|
||||
permalinkPrefix={ permalinkPrefix || '' }
|
||||
permalinkSuffix={ permalinkSuffix || '' }
|
||||
product={ values }
|
||||
onCancel={ () => setShowProductLinkEditModal( false ) }
|
||||
onSaved={ () => setShowProductLinkEditModal( false ) }
|
||||
/>
|
||||
) }
|
||||
</ProductSectionLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './edit-product-link-modal';
|
|
@ -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();
|
||||
} );
|
||||
} );
|
|
@ -177,6 +177,7 @@ describe( 'ProductFormActions', () => {
|
|||
);
|
||||
queryByText( 'Save draft' )?.click();
|
||||
expect( updateProductWithStatus ).toHaveBeenCalledWith(
|
||||
product.id,
|
||||
{ ...product, name: 'Name Update' },
|
||||
'draft'
|
||||
);
|
||||
|
@ -219,6 +220,7 @@ describe( 'ProductFormActions', () => {
|
|||
manage_stock: true,
|
||||
} );
|
||||
expect( updateProductWithStatus ).toHaveBeenCalledWith(
|
||||
product.id,
|
||||
product,
|
||||
'publish'
|
||||
);
|
||||
|
@ -367,6 +369,7 @@ describe( 'ProductFormActions', () => {
|
|||
);
|
||||
updateProductWithStatus.mockReturnValue( Promise.resolve() );
|
||||
expect( updateProductWithStatus ).toHaveBeenCalledWith(
|
||||
product.id,
|
||||
product,
|
||||
'publish'
|
||||
);
|
||||
|
@ -404,6 +407,7 @@ describe( 'ProductFormActions', () => {
|
|||
} );
|
||||
updateProductWithStatus.mockReturnValue( Promise.resolve() );
|
||||
expect( updateProductWithStatus ).toHaveBeenCalledWith(
|
||||
product.id,
|
||||
product,
|
||||
'publish'
|
||||
);
|
||||
|
|
|
@ -107,12 +107,20 @@ export function useProductHelper() {
|
|||
}
|
||||
},
|
||||
() => {
|
||||
if ( ! skipNotice ) {
|
||||
createNotice(
|
||||
'error',
|
||||
status === 'publish'
|
||||
? __( 'Failed to publish product.', 'woocommerce' )
|
||||
: __( 'Failed to create product.', 'woocommerce' )
|
||||
? __(
|
||||
'Failed to publish product.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Failed to create product.',
|
||||
'woocommerce'
|
||||
)
|
||||
);
|
||||
}
|
||||
setUpdating( {
|
||||
...updating,
|
||||
[ status ]: false,
|
||||
|
@ -126,14 +134,16 @@ export function useProductHelper() {
|
|||
/**
|
||||
* 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 {boolean} skipNotice if the notice should be skipped (default: false).
|
||||
* @return {Promise<Product>} Returns a promise with the updated product.
|
||||
*/
|
||||
const updateProductWithStatus = useCallback(
|
||||
async (
|
||||
product: Product,
|
||||
productId: number,
|
||||
product: Partial< Product >,
|
||||
status: ProductStatus,
|
||||
skipNotice = false
|
||||
): Promise< Product > => {
|
||||
|
@ -141,7 +151,7 @@ export function useProductHelper() {
|
|||
...updating,
|
||||
[ status ]: true,
|
||||
} );
|
||||
return updateProduct( product.id, {
|
||||
return updateProduct( productId, {
|
||||
...product,
|
||||
status,
|
||||
} ).then(
|
||||
|
@ -174,10 +184,12 @@ export function useProductHelper() {
|
|||
return updatedProduct;
|
||||
},
|
||||
( error ) => {
|
||||
if ( ! skipNotice ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Failed to update product.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
setUpdating( {
|
||||
...updating,
|
||||
[ status ]: false,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add product link field to the new edit product form.
|
Loading…
Reference in New Issue