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
This commit is contained in:
parent
51c33fa351
commit
e1aabf2d9d
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update attributes type for product variations data store
|
|
@ -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' >;
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './variations';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<Card className="woocommerce-product-variations is-loading">
|
||||
<Spinner />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-product-variations">
|
||||
<div className="woocommerce-product-variations__header">
|
||||
<h4>{ __( 'Variation', 'woocommerce' ) }</h4>
|
||||
<h4>
|
||||
{ sprintf(
|
||||
/** Translators: The 3 letter currency code for the store. */
|
||||
__( 'Price (%s)', 'woocommerce' ),
|
||||
currencyConfig.code
|
||||
) }
|
||||
</h4>
|
||||
<h4>{ __( 'Quantity', 'woocommerce' ) }</h4>
|
||||
</div>
|
||||
<Sortable>
|
||||
{ variations.map( ( variation ) => (
|
||||
<ListItem key={ variation.id }>
|
||||
<div className="woocommerce-product-variations__attributes">
|
||||
{ variation.attributes.map( ( attribute ) => (
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
/* @ts-ignore Additional props are not required. */
|
||||
<Tag
|
||||
id={ attribute.id }
|
||||
className="woocommerce-product-variations__attribute"
|
||||
key={ attribute.id }
|
||||
label={ attribute.option }
|
||||
/>
|
||||
) ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__price">
|
||||
{ formatAmount( variation.price ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__quantity">
|
||||
{ getProductStockStatus( variation ) }
|
||||
</div>
|
||||
</ListItem>
|
||||
) ) }
|
||||
</Sortable>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -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< {
|
|||
<ProductFormTab name="shipping" title="Shipping">
|
||||
<ProductShippingSection product={ product } />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="options" title="Options">
|
||||
<ProductVariationsSection />
|
||||
</ProductFormTab>
|
||||
</ProductFormLayout>
|
||||
<ProductFormFooter />
|
||||
</Form>
|
||||
|
|
|
@ -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 (
|
||||
<ProductSectionLayout
|
||||
title={ __( 'Variations', 'woocommerce' ) }
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{ __(
|
||||
'Manage individual product combinations created from options.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
<Link
|
||||
className="woocommerce-form-section__header-link"
|
||||
href="https://woocommerce.com/posts/product-variations-display/"
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent( 'add_product_variation_help' );
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'How to make variations work for you',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Variations />
|
||||
</ProductSectionLayout>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add product variations list to new product management experience
|
Loading…
Reference in New Issue