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:
Maikel David Pérez Gómez 2023-08-25 12:13:16 -04:00 committed by GitHub
parent ed0d45178f
commit 23f33a6b74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 531 additions and 104 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add Pricing item to the Quick Actions menu per Variation item

View File

@ -0,0 +1,2 @@
export * from './variation-actions-menu';
export * from './types';

View File

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

View File

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

View File

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