Add Pricing item to the Quick Actions menu per Variation item (#39872)
* Move the Quick Actions menu to its own component * Add Set list price item to the Pricing sub menu * Add notice when a variation gets updated * Add Set sale price to Pricing sub menu * Do not fire change if the user cancel the window propmt * Add Increase list price to Pricing sub menu * Add Decrease list price to Pricing sub menu * Add Increase sale price to Pricing sub menu * Add Decrease sale price to Pricing sub menu * Add Schedule sale to Pricing sub menu * Add changelog file * Fix lint error * Fix typo * Add Sale end date to the Schedule sale to Pricing sub menu * Add tracking events * Fix tracking event params from camelcase to snakecase
This commit is contained in:
parent
ed0d45178f
commit
23f33a6b74
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add Pricing item to the Quick Actions menu per Variation item
|
|
@ -0,0 +1,2 @@
|
|||
export * from './variation-actions-menu';
|
||||
export * from './types';
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
|
||||
export type VariationActionsMenuProps = {
|
||||
variation: ProductVariation;
|
||||
onChange( variation: Partial< ProductVariation > ): void;
|
||||
onDelete( variationId: number ): void;
|
||||
};
|
|
@ -0,0 +1,466 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
} from '@wordpress/components';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { chevronRight, moreVertical } from '@wordpress/icons';
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { VariationActionsMenuProps } from './types';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
|
||||
function isPercentage( value: string ) {
|
||||
return value.endsWith( '%' );
|
||||
}
|
||||
|
||||
function parsePercentage( value: string ) {
|
||||
const stringNumber = value.substring( 0, value.length - 1 );
|
||||
if ( Number.isNaN( Number( stringNumber ) ) ) {
|
||||
return undefined;
|
||||
}
|
||||
return Number( stringNumber );
|
||||
}
|
||||
|
||||
function addFixedOrPercentage(
|
||||
value: string,
|
||||
fixedOrPercentage: string,
|
||||
increaseOrDecrease: 1 | -1 = 1
|
||||
) {
|
||||
if ( isPercentage( fixedOrPercentage ) ) {
|
||||
if ( Number.isNaN( Number( value ) ) ) {
|
||||
return 0;
|
||||
}
|
||||
const percentage = parsePercentage( fixedOrPercentage );
|
||||
if ( percentage === undefined ) {
|
||||
return Number( value );
|
||||
}
|
||||
return (
|
||||
Number( value ) +
|
||||
Number( value ) * ( percentage / 100 ) * increaseOrDecrease
|
||||
);
|
||||
}
|
||||
if ( Number.isNaN( Number( value ) ) ) {
|
||||
if ( Number.isNaN( Number( fixedOrPercentage ) ) ) {
|
||||
return undefined;
|
||||
}
|
||||
return Number( fixedOrPercentage );
|
||||
}
|
||||
return Number( value ) + Number( fixedOrPercentage ) * increaseOrDecrease;
|
||||
}
|
||||
|
||||
export function VariationActionsMenu( {
|
||||
variation,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: VariationActionsMenuProps ) {
|
||||
function handlePrompt(
|
||||
propertyName: keyof ProductVariation,
|
||||
label: string = __( 'Enter a value', 'woocommerce' ),
|
||||
parser: ( value: string ) => unknown = ( value ) => value
|
||||
) {
|
||||
// eslint-disable-next-line no-alert
|
||||
const value = window.prompt( label );
|
||||
if ( value === null ) return;
|
||||
|
||||
onChange( {
|
||||
[ propertyName ]: parser( value.trim() ),
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
icon={ moreVertical }
|
||||
label={ __( 'Actions', 'woocommerce' ) }
|
||||
toggleProps={ {
|
||||
onClick() {
|
||||
recordEvent( 'product_variations_menu_view', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
} );
|
||||
},
|
||||
} }
|
||||
>
|
||||
{ ( { onClose } ) => (
|
||||
<>
|
||||
<MenuGroup
|
||||
label={ sprintf(
|
||||
/** Translators: Variation ID */
|
||||
__( 'Variation Id: %s', 'woocommerce' ),
|
||||
variation.id
|
||||
) }
|
||||
>
|
||||
<MenuItem
|
||||
href={ variation.permalink }
|
||||
onClick={ () => {
|
||||
recordEvent( 'product_variations_preview', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ __( 'Preview', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Dropdown
|
||||
position="middle right"
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_click',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
onToggle();
|
||||
} }
|
||||
aria-expanded={ isOpen }
|
||||
icon={ chevronRight }
|
||||
iconPosition="right"
|
||||
>
|
||||
{ __( 'Pricing', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
) }
|
||||
renderContent={ () => (
|
||||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup
|
||||
label={ __(
|
||||
'List price',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'regular_price',
|
||||
undefined,
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return value;
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Set list price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'regular_price',
|
||||
__(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return addFixedOrPercentage(
|
||||
variation.regular_price,
|
||||
value
|
||||
)?.toFixed( 2 );
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Increase list price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_decrease',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'regular_price',
|
||||
__(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return addFixedOrPercentage(
|
||||
variation.regular_price,
|
||||
value,
|
||||
-1
|
||||
)?.toFixed( 2 );
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Decrease list price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup
|
||||
label={ __(
|
||||
'Sale price',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_set',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'sale_price',
|
||||
undefined,
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_set',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return value;
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Set sale price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_increase',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'sale_price',
|
||||
__(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_increase',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return addFixedOrPercentage(
|
||||
variation.sale_price,
|
||||
value
|
||||
)?.toFixed( 2 );
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Increase sale price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_decrease',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'sale_price',
|
||||
__(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_decrease',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return addFixedOrPercentage(
|
||||
variation.sale_price,
|
||||
value,
|
||||
-1
|
||||
)?.toFixed( 2 );
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Decrease sale price',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'date_on_sale_from_gmt',
|
||||
__(
|
||||
'Sale start date (YYYY-MM-DD format or leave blank)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return value;
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
'date_on_sale_to_gmt',
|
||||
__(
|
||||
'Sale end date (YYYY-MM-DD format or leave blank)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id:
|
||||
variation.id,
|
||||
}
|
||||
);
|
||||
return value;
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Schedule sale',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
variant="link"
|
||||
onClick={ () => {
|
||||
onDelete( variation.id );
|
||||
onClose();
|
||||
} }
|
||||
className="woocommerce-product-variations__actions--delete"
|
||||
>
|
||||
{ __( 'Delete', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</>
|
||||
) }
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
|
@ -2,28 +2,15 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
} from '@wordpress/components';
|
||||
import { Button, Spinner, Tooltip } from '@wordpress/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
ProductVariation,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components';
|
||||
import {
|
||||
useContext,
|
||||
useState,
|
||||
createElement,
|
||||
Fragment,
|
||||
} from '@wordpress/element';
|
||||
import { useContext, useState, createElement } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { moreVertical } from '@wordpress/icons';
|
||||
import classnames from 'classnames';
|
||||
import truncate from 'lodash/truncate';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
|
@ -43,6 +30,7 @@ import {
|
|||
PRODUCT_VARIATION_TITLE_LIMIT,
|
||||
TRACKS_SOURCE,
|
||||
} from '../../constants';
|
||||
import { VariationActionsMenu } from './variation-actions-menu';
|
||||
|
||||
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||
const VISIBLE_TEXT = __( 'Visible to customers', 'woocommerce' );
|
||||
|
@ -99,6 +87,9 @@ export function VariationsTable() {
|
|||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( 'core/notices' );
|
||||
|
||||
if ( ! variations && isLoading ) {
|
||||
return (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
|
@ -112,26 +103,6 @@ export function VariationsTable() {
|
|||
);
|
||||
}
|
||||
|
||||
function handleCustomerVisibilityClick(
|
||||
variationId: number,
|
||||
status: 'private' | 'publish'
|
||||
) {
|
||||
if ( isUpdating[ variationId ] ) return;
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: true,
|
||||
} ) );
|
||||
updateProductVariation< Promise< ProductVariation > >(
|
||||
{ product_id: productId, id: variationId },
|
||||
{ status }
|
||||
).finally( () =>
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: false,
|
||||
} ) )
|
||||
);
|
||||
}
|
||||
|
||||
function handleDeleteVariationClick( variationId: number ) {
|
||||
if ( isUpdating[ variationId ] ) return;
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
|
@ -155,6 +126,38 @@ export function VariationsTable() {
|
|||
);
|
||||
}
|
||||
|
||||
function handleVariationChange(
|
||||
variationId: number,
|
||||
variation: Partial< ProductVariation >
|
||||
) {
|
||||
if ( isUpdating[ variationId ] ) return;
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: true,
|
||||
} ) );
|
||||
updateProductVariation< Promise< ProductVariation > >(
|
||||
{ product_id: productId, id: variationId },
|
||||
variation
|
||||
)
|
||||
.then( () => {
|
||||
createSuccessNotice(
|
||||
/* translators: The updated variations count */
|
||||
sprintf( __( '%s variation/s updated.', 'woocommerce' ), 1 )
|
||||
);
|
||||
} )
|
||||
.catch( () => {
|
||||
createErrorNotice(
|
||||
__( 'Failed to save variation.', 'woocommerce' )
|
||||
);
|
||||
} )
|
||||
.finally( () =>
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: false,
|
||||
} ) )
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-product-variations">
|
||||
{ isLoading ||
|
||||
|
@ -251,9 +254,9 @@ export function VariationsTable() {
|
|||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleCustomerVisibilityClick(
|
||||
handleVariationChange(
|
||||
variation.id,
|
||||
'publish'
|
||||
{ status: 'publish' }
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -282,9 +285,9 @@ export function VariationsTable() {
|
|||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleCustomerVisibilityClick(
|
||||
handleVariationChange(
|
||||
variation.id,
|
||||
'private'
|
||||
{ status: 'private' }
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -296,71 +299,13 @@ export function VariationsTable() {
|
|||
</Button>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
<DropdownMenu
|
||||
icon={ moreVertical }
|
||||
label={ __( 'Actions', 'woocommerce' ) }
|
||||
toggleProps={ {
|
||||
onClick() {
|
||||
recordEvent(
|
||||
'product_variations_menu_view',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
}
|
||||
);
|
||||
},
|
||||
} }
|
||||
>
|
||||
{ ( { onClose } ) => (
|
||||
<>
|
||||
<MenuGroup
|
||||
label={ sprintf(
|
||||
/** Translators: Variation ID */
|
||||
__(
|
||||
'Variation Id: %s',
|
||||
'woocommerce'
|
||||
),
|
||||
variation.id
|
||||
) }
|
||||
>
|
||||
<MenuItem
|
||||
href={ variation.permalink }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_preview',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Preview',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
variant="link"
|
||||
onClick={ () => {
|
||||
handleDeleteVariationClick(
|
||||
variation.id
|
||||
);
|
||||
onClose();
|
||||
} }
|
||||
className="woocommerce-product-variations__actions--delete"
|
||||
>
|
||||
{ __(
|
||||
'Delete',
|
||||
'woocommerce'
|
||||
) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</>
|
||||
) }
|
||||
</DropdownMenu>
|
||||
<VariationActionsMenu
|
||||
variation={ variation }
|
||||
onChange={ ( value ) =>
|
||||
handleVariationChange( variation.id, value )
|
||||
}
|
||||
onDelete={ handleDeleteVariationClick }
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
) ) }
|
||||
|
|
Loading…
Reference in New Issue