diff --git a/packages/js/components/changelog/add-34657-add-new-shipping-class b/packages/js/components/changelog/add-34657-add-new-shipping-class new file mode 100644 index 00000000000..3a6e304d072 --- /dev/null +++ b/packages/js/components/changelog/add-34657-add-new-shipping-class @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add new shippping class modal to a shipping class section in product page diff --git a/packages/js/components/src/form/form.tsx b/packages/js/components/src/form/form.tsx index 51f2ca147c2..4298978038c 100644 --- a/packages/js/components/src/form/form.tsx +++ b/packages/js/components/src/form/form.tsx @@ -262,7 +262,7 @@ function FormComponent< Values extends Record< string, any > >( } if ( callback ) { - callback( values ); + return callback( values ); } } }; diff --git a/packages/js/data/changelog/add-34657-add-new-shipping-class b/packages/js/data/changelog/add-34657-add-new-shipping-class new file mode 100644 index 00000000000..3a6e304d072 --- /dev/null +++ b/packages/js/data/changelog/add-34657-add-new-shipping-class @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add new shippping class modal to a shipping class section in product page diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts index 136b6638bd0..c7cc415c422 100644 --- a/packages/js/data/src/products/types.ts +++ b/packages/js/data/src/products/types.ts @@ -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; +}; diff --git a/plugins/woocommerce-admin/client/products/edit-product-page.tsx b/plugins/woocommerce-admin/client/products/edit-product-page.tsx index 6b6a39da9d8..051468daa0f 100644 --- a/plugins/woocommerce-admin/client/products/edit-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/edit-product-page.tsx @@ -127,7 +127,7 @@ const EditProductPage: React.FC = () => { - + diff --git a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx index 0a2626a8464..d4e47c56233 100644 --- a/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx +++ b/plugins/woocommerce-admin/client/products/sections/product-shipping-section.tsx @@ -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 ? ( <> { + if ( + value === + ADD_NEW_SHIPPING_CLASS_OPTION_VALUE + ) { + setShowShippingClassModal( true ); + return; + } + selectShippingClassProps?.onChange( value ); + } } /> { interpolateComponents( { @@ -309,6 +364,24 @@ export const ProductShippingSection: React.FC = () => { ) } + + { showShippingClassModal && ( + + createProductShippingClass< + Promise< ProductShippingClass > + >( values ).then( ( value ) => { + invalidateResolution( 'getProductShippingClasses' ); + return value; + } ) + } + onCancel={ () => setShowShippingClassModal( false ) } + /> + ) } ); -}; +} diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.scss b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.scss new file mode 100644 index 00000000000..03b4be4beb0 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.scss @@ -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; + } + } +} diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx new file mode 100644 index 00000000000..080e77127c5 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/add-new-shipping-class-modal.tsx @@ -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 ( +
+ + + ), + }, + } ) } + /> + + ), + }, + } ) } + help={ + inputDescriptionProps?.help ?? + __( + 'Describe how you and other store administrators can use this shipping class.', + 'woocommerce' + ) + } + /> +
+ + +
+
+ ); +} + +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 ( + + > + initialValues={ shippingClass ?? INITIAL_VALUES } + validate={ validateForm } + errors={ {} } + onSubmit={ onAdd } + > + { ( childrenProps: { + handleSubmit: () => Promise< ProductShippingClass >; + } ) => ( + + ) } + + + ); +} diff --git a/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/index.ts b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/index.ts new file mode 100644 index 00000000000..3d143c44ae1 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/shared/add-new-shipping-class-modal/index.ts @@ -0,0 +1 @@ +export * from './add-new-shipping-class-modal'; diff --git a/plugins/woocommerce/changelog/add-34657-add-new-shipping-class b/plugins/woocommerce/changelog/add-34657-add-new-shipping-class new file mode 100644 index 00000000000..3a6e304d072 --- /dev/null +++ b/plugins/woocommerce/changelog/add-34657-add-new-shipping-class @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add new shippping class modal to a shipping class section in product page