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:
Maikel David Pérez Gómez 2022-10-11 12:00:35 -03:00 committed by GitHub
parent 1d4888768f
commit e95bb3768e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 281 additions and 11 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add new shippping class modal to a shipping class section in product page

View File

@ -262,7 +262,7 @@ function FormComponent< Values extends Record< string, any > >(
}
if ( callback ) {
callback( values );
return callback( values );
}
}
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add new shippping class modal to a shipping class section in product page

View File

@ -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;
};

View File

@ -127,7 +127,7 @@ const EditProductPage: React.FC = () => {
<ProductFormLayout>
<ProductDetailsSection />
<PricingSection />
<ProductShippingSection />
<ProductShippingSection product={ product } />
<AttributesSection />
<ProductFormActions />
</ProductFormLayout>

View File

@ -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>
);
};
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}

View File

@ -0,0 +1 @@
export * from './add-new-shipping-class-modal';

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add new shippping class modal to a shipping class section in product page