Add new shipping class modal to a shipping class section in product page (#34937)
Add new shippping class modal to a shipping class section in product page
This commit is contained in:
parent
1d4888768f
commit
e95bb3768e
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add new shippping class modal to a shipping class section in product page
|
|
@ -262,7 +262,7 @@ function FormComponent< Values extends Record< string, any > >(
|
|||
}
|
||||
|
||||
if ( callback ) {
|
||||
callback( values );
|
||||
return callback( values );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add new shippping class modal to a shipping class section in product page
|
|
@ -36,7 +36,7 @@ export type ProductAttribute = {
|
|||
|
||||
export type Product< Status = ProductStatus, Type = ProductType > = Omit<
|
||||
Schema.Post,
|
||||
'status'
|
||||
'status' | 'categories'
|
||||
> & {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -88,6 +88,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit<
|
|||
attributes: ProductAttribute[];
|
||||
dimensions: ProductDimensions;
|
||||
weight: string;
|
||||
categories: ProductCategory[];
|
||||
};
|
||||
|
||||
export const productReadOnlyProperties = [
|
||||
|
@ -152,3 +153,9 @@ export type ProductDimensions = {
|
|||
height: string;
|
||||
length: string;
|
||||
};
|
||||
|
||||
export type ProductCategory = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
|
|
@ -127,7 +127,7 @@ const EditProductPage: React.FC = () => {
|
|||
<ProductFormLayout>
|
||||
<ProductDetailsSection />
|
||||
<PricingSection />
|
||||
<ProductShippingSection />
|
||||
<ProductShippingSection product={ product } />
|
||||
<AttributesSection />
|
||||
<ProductFormActions />
|
||||
</ProductFormLayout>
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Link, Spinner, useFormContext } from '@woocommerce/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME,
|
||||
OPTIONS_STORE_NAME,
|
||||
Product,
|
||||
PartialProduct,
|
||||
ProductShippingClass,
|
||||
} from '@woocommerce/data';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
|
@ -31,13 +31,27 @@ import {
|
|||
ShippingDimensionsImageProps,
|
||||
} from '../fields/shipping-dimensions-image';
|
||||
import { useProductHelper } from '../use-product-helper';
|
||||
import { AddNewShippingClassModal } from '../shared/add-new-shipping-class-modal';
|
||||
import { getTextControlProps } from './utils';
|
||||
import './product-shipping-section.scss';
|
||||
|
||||
export type ProductShippingSectionProps = {
|
||||
product?: PartialProduct;
|
||||
};
|
||||
|
||||
// This should never be a real slug value of any existing shipping class
|
||||
const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = '__ADD_NEW_SHIPPING_CLASS_OPTION__';
|
||||
|
||||
const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [
|
||||
{ value: '', label: __( 'No shipping class', 'woocommerce' ) },
|
||||
{
|
||||
value: ADD_NEW_SHIPPING_CLASS_OPTION_VALUE,
|
||||
label: __( 'Add new shipping class', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
|
||||
const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized';
|
||||
|
||||
function mapShippingClassToSelectOption(
|
||||
shippingClasses: ProductShippingClass[]
|
||||
): SelectControl.Option[] {
|
||||
|
@ -56,11 +70,37 @@ function getInterpolatedSizeLabel( mixedString: string ) {
|
|||
} );
|
||||
}
|
||||
|
||||
export const ProductShippingSection: React.FC = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
/**
|
||||
* This extracts a shipping class from the product categories. Using
|
||||
* the first category different to `Uncategorized`.
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce/issues/34657
|
||||
* @param product The product
|
||||
* @return The default shipping class
|
||||
*/
|
||||
function extractDefaultShippingClassFromProduct(
|
||||
product: PartialProduct
|
||||
): Partial< ProductShippingClass > | undefined {
|
||||
const category = product?.categories?.find(
|
||||
( { slug } ) => slug !== UNCATEGORIZED_CATEGORY_SLUG
|
||||
);
|
||||
if ( category ) {
|
||||
return {
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function ProductShippingSection( {
|
||||
product,
|
||||
}: ProductShippingSectionProps ) {
|
||||
const { getInputProps } = useFormContext< PartialProduct >();
|
||||
const { formatNumber, parseNumber } = useProductHelper();
|
||||
const [ highlightSide, setHighlightSide ] =
|
||||
useState< ShippingDimensionsImageProps[ 'highlight' ] >();
|
||||
const [ showShippingClassModal, setShowShippingClassModal ] =
|
||||
useState( false );
|
||||
|
||||
const { shippingClasses, hasResolvedShippingClasses } = useSelect(
|
||||
( select ) => {
|
||||
|
@ -97,6 +137,13 @@ export const ProductShippingSection: React.FC = () => {
|
|||
[]
|
||||
);
|
||||
|
||||
const { createProductShippingClass, invalidateResolution } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
|
||||
);
|
||||
|
||||
const selectShippingClassProps = getTextControlProps(
|
||||
getInputProps( 'shipping_class' )
|
||||
);
|
||||
const inputWidthProps = getTextControlProps(
|
||||
getInputProps( 'dimensions.width' )
|
||||
);
|
||||
|
@ -121,16 +168,24 @@ export const ProductShippingSection: React.FC = () => {
|
|||
{ hasResolvedShippingClasses ? (
|
||||
<>
|
||||
<SelectControl
|
||||
{ ...selectShippingClassProps }
|
||||
label={ __( 'Shipping class', 'woocommerce' ) }
|
||||
{ ...getTextControlProps(
|
||||
getInputProps( 'shipping_class' )
|
||||
) }
|
||||
options={ [
|
||||
...DEFAULT_SHIPPING_CLASS_OPTIONS,
|
||||
...mapShippingClassToSelectOption(
|
||||
shippingClasses ?? []
|
||||
),
|
||||
] }
|
||||
onChange={ ( value: string ) => {
|
||||
if (
|
||||
value ===
|
||||
ADD_NEW_SHIPPING_CLASS_OPTION_VALUE
|
||||
) {
|
||||
setShowShippingClassModal( true );
|
||||
return;
|
||||
}
|
||||
selectShippingClassProps?.onChange( value );
|
||||
} }
|
||||
/>
|
||||
<span className="woocommerce-product-form__secondary-text">
|
||||
{ interpolateComponents( {
|
||||
|
@ -309,6 +364,24 @@ export const ProductShippingSection: React.FC = () => {
|
|||
) }
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{ showShippingClassModal && (
|
||||
<AddNewShippingClassModal
|
||||
shippingClass={
|
||||
product &&
|
||||
extractDefaultShippingClassFromProduct( product )
|
||||
}
|
||||
onAdd={ ( values ) =>
|
||||
createProductShippingClass<
|
||||
Promise< ProductShippingClass >
|
||||
>( values ).then( ( value ) => {
|
||||
invalidateResolution( 'getProductShippingClasses' );
|
||||
return value;
|
||||
} )
|
||||
}
|
||||
onCancel={ () => setShowShippingClassModal( false ) }
|
||||
/>
|
||||
) }
|
||||
</ProductSectionLayout>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
.woocommerce-add-new-shipping-class-modal {
|
||||
min-width: 650px;
|
||||
&__optional-input {
|
||||
color: $gray-700;
|
||||
}
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.has-error {
|
||||
.components-base-control__help {
|
||||
color: $studio-red-50;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Form, FormErrors, useFormContext } from '@woocommerce/components';
|
||||
import { ProductShippingClass } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getTextControlProps } from '../../sections/utils';
|
||||
import './add-new-shipping-class-modal.scss';
|
||||
|
||||
export type ShippingClassFormProps = {
|
||||
onAdd: () => Promise< ProductShippingClass >;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
function ShippingClassForm( { onAdd, onCancel }: ShippingClassFormProps ) {
|
||||
const { getInputProps, isValidForm } =
|
||||
useFormContext< ProductShippingClass >();
|
||||
const [ isLoading, setIsLoading ] = useState( false );
|
||||
|
||||
const inputNameProps = getTextControlProps( getInputProps( 'name' ) );
|
||||
const inputSlugProps = getTextControlProps( getInputProps( 'slug' ) );
|
||||
const inputDescriptionProps = getTextControlProps(
|
||||
getInputProps( 'description' )
|
||||
);
|
||||
|
||||
function handleAdd() {
|
||||
setIsLoading( true );
|
||||
onAdd()
|
||||
.then( () => {
|
||||
setIsLoading( false );
|
||||
onCancel();
|
||||
} )
|
||||
.catch( () => {
|
||||
setIsLoading( false );
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-add-new-shipping-class-modal__wrapper">
|
||||
<TextControl
|
||||
{ ...inputNameProps }
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
/>
|
||||
<TextControl
|
||||
{ ...inputSlugProps }
|
||||
label={ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Slug {{span}}(optional){{/span}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
span: (
|
||||
<span className="woocommerce-add-new-shipping-class-modal__optional-input" />
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
/>
|
||||
<TextControl
|
||||
{ ...inputDescriptionProps }
|
||||
label={ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Description {{span}}(optional){{/span}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
span: (
|
||||
<span className="woocommerce-add-new-shipping-class-modal__optional-input" />
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
help={
|
||||
inputDescriptionProps?.help ??
|
||||
__(
|
||||
'Describe how you and other store administrators can use this shipping class.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="woocommerce-add-new-shipping-class-modal__buttons">
|
||||
<Button isSecondary onClick={ onCancel }>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isLoading }
|
||||
disabled={ ! isValidForm || isLoading }
|
||||
onClick={ handleAdd }
|
||||
>
|
||||
{ __( 'Add', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function validateForm(
|
||||
values: Partial< ProductShippingClass >
|
||||
): FormErrors< ProductShippingClass > {
|
||||
const errors: FormErrors< ProductShippingClass > = {};
|
||||
|
||||
if ( ! values.name?.length ) {
|
||||
errors.name = __(
|
||||
'The shipping class name is required.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export type AddNewShippingClassModalProps = {
|
||||
shippingClass?: Partial< ProductShippingClass >;
|
||||
onAdd: (
|
||||
shippingClass: Partial< ProductShippingClass >
|
||||
) => Promise< ProductShippingClass >;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
name: __( 'New shipping class', 'woocommerce' ),
|
||||
slug: __( 'new-shipping-class', 'woocommerce' ),
|
||||
};
|
||||
|
||||
export function AddNewShippingClassModal( {
|
||||
shippingClass,
|
||||
onAdd,
|
||||
onCancel,
|
||||
}: AddNewShippingClassModalProps ) {
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'New shipping class', 'woocommerce' ) }
|
||||
className="woocommerce-add-new-shipping-class-modal"
|
||||
onRequestClose={ onCancel }
|
||||
>
|
||||
<Form< Partial< ProductShippingClass > >
|
||||
initialValues={ shippingClass ?? INITIAL_VALUES }
|
||||
validate={ validateForm }
|
||||
errors={ {} }
|
||||
onSubmit={ onAdd }
|
||||
>
|
||||
{ ( childrenProps: {
|
||||
handleSubmit: () => Promise< ProductShippingClass >;
|
||||
} ) => (
|
||||
<ShippingClassForm
|
||||
onAdd={ childrenProps.handleSubmit }
|
||||
onCancel={ onCancel }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './add-new-shipping-class-modal';
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Add new shippping class modal to a shipping class section in product page
|
Loading…
Reference in New Issue