From e1aabf2d9d84141da5600d1920ea06492c66d016 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 13 Dec 2022 15:29:05 -0800 Subject: [PATCH] Add product variations list to new product management experience (#35889) * Add product variations section * Add variations list * Add util to get product stock status * Add variation specific attribute type * Add currency code to header column * Fix up variations header width * Add variations loading state * Add changelog entries * Convert spaces to tabs * Fix status typo * Fix up return type for stock status --- packages/js/data/changelog/add-35772 | 4 + .../js/data/src/product-variations/types.ts | 13 ++- .../products/fields/variations/index.ts | 1 + .../fields/variations/variations.scss | 39 ++++++++ .../products/fields/variations/variations.tsx | 91 +++++++++++++++++++ .../client/products/product-form.tsx | 4 + .../sections/product-variations-section.tsx | 46 ++++++++++ .../products/utils/get-product-status.ts | 2 +- .../utils/get-product-stock-status.ts | 49 ++++++++++ plugins/woocommerce/changelog/add-35772 | 4 + 10 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 packages/js/data/changelog/add-35772 create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/index.ts create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/variations.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/variations.tsx create mode 100644 plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx create mode 100644 plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts create mode 100644 plugins/woocommerce/changelog/add-35772 diff --git a/packages/js/data/changelog/add-35772 b/packages/js/data/changelog/add-35772 new file mode 100644 index 00000000000..289817caff8 --- /dev/null +++ b/packages/js/data/changelog/add-35772 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update attributes type for product variations data store diff --git a/packages/js/data/src/product-variations/types.ts b/packages/js/data/src/product-variations/types.ts index 69a85bd8f04..2369bce1d8c 100644 --- a/packages/js/data/src/product-variations/types.ts +++ b/packages/js/data/src/product-variations/types.ts @@ -9,7 +9,18 @@ import { DispatchFromMap } from '@automattic/data-stores'; import { CrudActions, CrudSelectors } from '../crud/types'; import { Product, ProductQuery, ReadOnlyProperties } from '../products/types'; -export type ProductVariation = Omit< Product, 'name' | 'slug' >; +export type ProductVariationAttribute = { + id: number; + name: string; + option: string; +}; + +export type ProductVariation = Omit< + Product, + 'name' | 'slug' | 'attributes' +> & { + attributes: ProductVariationAttribute[]; +}; type Query = Omit< ProductQuery, 'name' >; diff --git a/plugins/woocommerce-admin/client/products/fields/variations/index.ts b/plugins/woocommerce-admin/client/products/fields/variations/index.ts new file mode 100644 index 00000000000..ec5dc0ca358 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/index.ts @@ -0,0 +1 @@ +export * from './variations'; diff --git a/plugins/woocommerce-admin/client/products/fields/variations/variations.scss b/plugins/woocommerce-admin/client/products/fields/variations/variations.scss new file mode 100644 index 00000000000..920aa7db6c3 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/variations.scss @@ -0,0 +1,39 @@ +.woocommerce-product-variations { + min-height: 300px; + + &__header { + display: grid; + grid-template-columns: calc(38px + 25%) 25% 25%; + padding: $gap-small $gap; + + h4 { + color: $gray-700; + font-size: 11px; + text-transform: uppercase; + margin: 0; + font-weight: 500; + } + } + + .woocommerce-list-item { + display: grid; + grid-template-columns: 38px 25% 25% 25%; + margin-left: -1px; + margin-right: -1px; + margin-bottom: -1px; + } + + .woocommerce-sortable { + margin: 0; + } + + .components-spinner { + width: 34px; + height: 34px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + position: absolute; + margin: 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx b/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx new file mode 100644 index 00000000000..3df18ef77de --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Card, Spinner } from '@wordpress/components'; +import { + EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, + ProductVariation, +} from '@woocommerce/data'; +import { ListItem, Sortable, Tag } from '@woocommerce/components'; +import { useContext } from '@wordpress/element'; +import { useParams } from 'react-router-dom'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { CurrencyContext } from '../../../lib/currency-context'; +import { getProductStockStatus } from '../../utils/get-product-stock-status'; +import './variations.scss'; + +export const Variations: React.FC = () => { + const { productId } = useParams(); + const context = useContext( CurrencyContext ); + const { formatAmount, getCurrencyConfig } = context; + const { isLoading, variations } = useSelect( ( select ) => { + const { getProductVariations, hasFinishedResolution } = select( + EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME + ); + return { + isLoading: ! hasFinishedResolution( 'getProductVariations', [ + { + product_id: productId, + }, + ] ), + variations: getProductVariations< ProductVariation[] >( { + product_id: productId, + } ), + }; + } ); + + if ( ! variations || isLoading ) { + return ( + + + + ); + } + + const currencyConfig = getCurrencyConfig(); + + return ( + +
+

{ __( 'Variation', 'woocommerce' ) }

+

+ { sprintf( + /** Translators: The 3 letter currency code for the store. */ + __( 'Price (%s)', 'woocommerce' ), + currencyConfig.code + ) } +

+

{ __( 'Quantity', 'woocommerce' ) }

+
+ + { variations.map( ( variation ) => ( + +
+ { variation.attributes.map( ( attribute ) => ( + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /* @ts-ignore Additional props are not required. */ + + ) ) } +
+
+ { formatAmount( variation.price ) } +
+
+ { getProductStockStatus( variation ) } +
+
+ ) ) } +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx index adba8194aa5..d2a98a57ca5 100644 --- a/plugins/woocommerce-admin/client/products/product-form.tsx +++ b/plugins/woocommerce-admin/client/products/product-form.tsx @@ -14,6 +14,7 @@ import { ProductDetailsSection } from './sections/product-details-section'; import { ProductInventorySection } from './sections/product-inventory-section'; import { PricingSection } from './sections/pricing-section'; import { ProductShippingSection } from './sections/product-shipping-section'; +import { ProductVariationsSection } from './sections/product-variations-section'; import { ImagesSection } from './sections/images-section'; import './product-page.scss'; import { validate } from './product-validation'; @@ -56,6 +57,9 @@ export const ProductForm: React.FC< { + + + diff --git a/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx new file mode 100644 index 00000000000..a4719e372b6 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { recordEvent } from '@woocommerce/tracks'; +import { Link } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { ProductSectionLayout } from '../layout/product-section-layout'; +import { Variations } from '../fields/variations'; + +export const ProductVariationsSection: React.FC = () => { + return ( + + + { __( + 'Manage individual product combinations created from options.', + 'woocommerce' + ) } + + { + recordEvent( 'add_product_variation_help' ); + } } + > + { __( + 'How to make variations work for you', + 'woocommerce' + ) } + + + } + > + + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/utils/get-product-status.ts b/plugins/woocommerce-admin/client/products/utils/get-product-status.ts index 519a57b552e..d5ddb33f7b7 100644 --- a/plugins/woocommerce-admin/client/products/utils/get-product-status.ts +++ b/plugins/woocommerce-admin/client/products/utils/get-product-status.ts @@ -28,7 +28,7 @@ export const PRODUCT_STATUS_LABELS = { * Get the product status for use in the header. * * @param product Product instance. - * @return {PRODUCT_STATUS_KEYS} Product staus key. + * @return {PRODUCT_STATUS_KEYS} Product status key. */ export const getProductStatus = ( product: PartialProduct | undefined diff --git a/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts b/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts new file mode 100644 index 00000000000..39a29cd4b50 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PartialProduct, ProductVariation } from '@woocommerce/data'; + +/** + * Labels for product stock statuses. + */ +export enum PRODUCT_STOCK_STATUS_KEYS { + instock = 'instock', + onbackorder = 'onbackorder', + outofstock = 'outofstock', +} + +/** + * Labels for product stock statuses. + */ +export const PRODUCT_STOCK_STATUS_LABELS = { + [ PRODUCT_STOCK_STATUS_KEYS.instock ]: __( 'In stock', 'woocommerce' ), + [ PRODUCT_STOCK_STATUS_KEYS.onbackorder ]: __( + 'On backorder', + 'woocommerce' + ), + [ PRODUCT_STOCK_STATUS_KEYS.outofstock ]: __( + 'Out of stock', + 'woocommerce' + ), +}; + +/** + * Get the product stock quantity or stock status label. + * + * @param product Product instance. + * @return {PRODUCT_STOCK_STATUS_KEYS|number} Product stock quantity or product status key. + */ +export const getProductStockStatus = ( + product: PartialProduct | Partial< ProductVariation > +): string | number => { + if ( product.manage_stock ) { + return product.stock_quantity || 0; + } + + if ( product.stock_status ) { + return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ]; + } + + return PRODUCT_STOCK_STATUS_LABELS.instock; +}; diff --git a/plugins/woocommerce/changelog/add-35772 b/plugins/woocommerce/changelog/add-35772 new file mode 100644 index 00000000000..31bbe41bd70 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35772 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product variations list to new product management experience