[Product Editor] Variation quick actions succeed when sku is inherited from parent (#44017)

This commit is contained in:
Matt Sherman 2024-01-24 09:50:00 -05:00 committed by GitHub
parent 081f9d303b
commit 1ea439fb61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 322 additions and 454 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add PartialProductVariation type, where id is required

View File

@ -87,6 +87,7 @@ export * from './onboarding/types';
export * from './plugins/types'; export * from './plugins/types';
export * from './products/types'; export * from './products/types';
export type { export type {
PartialProductVariation,
ProductVariation, ProductVariation,
ProductVariationAttribute, ProductVariationAttribute,
ProductVariationImage, ProductVariationImage,

View File

@ -57,25 +57,29 @@ export interface ProductVariationImage {
export type ProductVariation = Omit< export type ProductVariation = Omit<
Product, Product,
'slug' | 'attributes' | 'images' | 'manage_stock' 'slug' | 'attributes' | 'images' | 'manage_stock'
> & { > &
attributes: ProductVariationAttribute[]; Pick< Product, 'id' > & {
/** attributes: ProductVariationAttribute[];
* Variation image data. /**
*/ * Variation image data.
image?: ProductVariationImage; */
/** image?: ProductVariationImage;
* Stock management at variation level. It can have a /**
* 'parent' value if the parent product is managing * Stock management at variation level. It can have a
* the stock at the time the variation was created. * 'parent' value if the parent product is managing
* * the stock at the time the variation was created.
* @default false *
*/ * @default false
manage_stock: boolean | 'parent'; */
/** manage_stock: boolean | 'parent';
* The product id this variation belongs to /**
*/ * The product id this variation belongs to
parent_id: number; */
}; parent_id: number;
};
export type PartialProductVariation = Partial< ProductVariation > &
Pick< ProductVariation, 'id' >;
type Query = Omit< ProductQuery, 'name' >; type Query = Omit< ProductQuery, 'name' >;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Variation quick actions update properly when variation inherits sku of parent; slot fills always deal with arrays

View File

@ -4,8 +4,8 @@
import { sprintf, __ } from '@wordpress/i18n'; import { sprintf, __ } from '@wordpress/i18n';
import { import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
PartialProductVariation,
Product, Product,
ProductVariation,
useUserPreferences, useUserPreferences,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { useWooBlockProps } from '@woocommerce/block-templates'; import { useWooBlockProps } from '@woocommerce/block-templates';
@ -115,14 +115,14 @@ export function Edit( {
); );
function onSetPrices( function onSetPrices(
handleUpdateAll: ( update: Partial< ProductVariation >[] ) => void handleUpdateAll: ( update: PartialProductVariation[] ) => void
) { ) {
recordEvent( 'product_variations_set_prices_select', { recordEvent( 'product_variations_set_prices_select', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
} ); } );
const productVariationsListPromise = resolveSelect( const productVariationsListPromise = resolveSelect(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
).getProductVariations< Pick< ProductVariation, 'id' >[] >( { ).getProductVariations< PartialProductVariation[] >( {
product_id: productId, product_id: productId,
order: 'asc', order: 'asc',
orderby: 'menu_order', orderby: 'menu_order',

View File

@ -31,15 +31,11 @@ export function DownloadsMenuItem( {
onClose, onClose,
supportsMultipleSelection = false, supportsMultipleSelection = false,
}: VariationActionsMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
const downloadsIds: number[] = ( const downloadsIds: number[] = selection[ 0 ].downloads.map(
Array.isArray( selection ) ( { id }: ProductDownload ) => Number.parseInt( id, 10 )
? selection[ 0 ].downloads );
: selection.downloads
).map( ( { id }: ProductDownload ) => Number.parseInt( id, 10 ) );
const [ uploadFilesModalOpen, setUploadFilesModalOpen ] = useState( false ); const [ uploadFilesModalOpen, setUploadFilesModalOpen ] = useState( false );
@ -55,16 +51,12 @@ export function DownloadsMenuItem( {
downloads, downloads,
}; };
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { ...partialVariation,
...partialVariation, id,
id, } ) )
} ) ) );
);
} else {
onChange( partialVariation );
}
recordEvent( 'product_variations_menu_downloads_update', { recordEvent( 'product_variations_menu_downloads_update', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -98,20 +90,13 @@ export function DownloadsMenuItem( {
handlePrompt( { handlePrompt( {
message, message,
onOk( value ) { onOk( value ) {
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
downloadable: true,
[ name ]: value,
} ) )
);
} else {
onChange( {
downloadable: true, downloadable: true,
[ name ]: value, [ name ]: value,
} ); } ) )
} );
recordEvent( 'product_variations_menu_downloads_update', { recordEvent( 'product_variations_menu_downloads_update', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: `${ name }_set`, action: `${ name }_set`,

View File

@ -23,9 +23,7 @@ export function InventoryMenuItem( {
onClose, onClose,
supportsMultipleSelection = false, supportsMultipleSelection = false,
}: VariationActionsMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
return ( return (
<Dropdown <Dropdown
@ -70,20 +68,14 @@ export function InventoryMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, manage_stock } ) => ( {
( { id, manage_stock } ) => ( { id,
id, manage_stock: ! manage_stock,
manage_stock: ! manage_stock, } )
} ) )
) );
);
} else {
onChange( {
manage_stock: ! selection.manage_stock,
} );
}
onClose(); onClose();
} } } }
> >
@ -99,22 +91,14 @@ export function InventoryMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
stock_status:
PRODUCT_STOCK_STATUS_KEYS.instock,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status: stock_status:
PRODUCT_STOCK_STATUS_KEYS.instock, PRODUCT_STOCK_STATUS_KEYS.instock,
manage_stock: false, manage_stock: false,
} ); } ) )
} );
onClose(); onClose();
} } } }
> >
@ -130,22 +114,14 @@ export function InventoryMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
stock_status:
PRODUCT_STOCK_STATUS_KEYS.outofstock,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status: stock_status:
PRODUCT_STOCK_STATUS_KEYS.outofstock, PRODUCT_STOCK_STATUS_KEYS.outofstock,
manage_stock: false, manage_stock: false,
} ); } ) )
} );
onClose(); onClose();
} } } }
> >
@ -164,22 +140,14 @@ export function InventoryMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
stock_status:
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status: stock_status:
PRODUCT_STOCK_STATUS_KEYS.onbackorder, PRODUCT_STOCK_STATUS_KEYS.onbackorder,
manage_stock: false, manage_stock: false,
} ); } ) )
} );
onClose(); onClose();
} } } }
> >
@ -212,22 +180,14 @@ export function InventoryMenuItem( {
if ( Number.isNaN( lowStockAmount ) ) { if ( Number.isNaN( lowStockAmount ) ) {
return null; return null;
} }
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
low_stock_amount:
lowStockAmount,
manage_stock: true,
} ) )
);
} else {
onChange( {
low_stock_amount: low_stock_amount:
lowStockAmount, lowStockAmount,
manage_stock: true, manage_stock: true,
} ); } ) )
} );
}, },
} ); } );
onClose(); onClose();

View File

@ -61,9 +61,7 @@ export function PricingMenuItem( {
onClose, onClose,
supportsMultipleSelection = false, supportsMultipleSelection = false,
}: VariationActionsMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
return ( return (
<Dropdown <Dropdown
@ -119,31 +117,18 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, regular_price } ) => ( {
( { id,
id, regular_price:
regular_price, addFixedOrPercentage(
} ) => ( { regular_price,
id, value
regular_price: )?.toFixed( 2 ),
addFixedOrPercentage( } )
regular_price, )
value );
)?.toFixed( 2 ),
} )
)
);
} else {
onChange( {
regular_price:
addFixedOrPercentage(
selection.regular_price,
value
)?.toFixed( 2 ),
} );
}
}, },
} ); } );
onClose(); onClose();
@ -175,33 +160,19 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, regular_price } ) => ( {
( { id,
id, regular_price:
regular_price, addFixedOrPercentage(
} ) => ( { regular_price,
id, value,
regular_price: -1
addFixedOrPercentage( )?.toFixed( 2 ),
regular_price, } )
value, )
-1 );
)?.toFixed( 2 ),
} )
)
);
} else {
onChange( {
regular_price:
addFixedOrPercentage(
selection.regular_price,
value,
-1
)?.toFixed( 2 ),
} );
}
}, },
} ); } );
onClose(); onClose();
@ -231,18 +202,12 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
sale_price: value,
} ) )
);
} else {
onChange( {
sale_price: value, sale_price: value,
} ); } ) )
} );
}, },
} ); } );
onClose(); onClose();
@ -274,31 +239,18 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, sale_price } ) => ( {
( { id,
id, sale_price:
sale_price, addFixedOrPercentage(
} ) => ( { sale_price,
id, value
sale_price: )?.toFixed( 2 ),
addFixedOrPercentage( } )
sale_price, )
value );
)?.toFixed( 2 ),
} )
)
);
} else {
onChange( {
sale_price:
addFixedOrPercentage(
selection.sale_price,
value
)?.toFixed( 2 ),
} );
}
}, },
} ); } );
onClose(); onClose();
@ -330,33 +282,19 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, sale_price } ) => ( {
( { id,
id, sale_price:
sale_price, addFixedOrPercentage(
} ) => ( { sale_price,
id, value,
sale_price: -1
addFixedOrPercentage( )?.toFixed( 2 ),
sale_price, } )
value, )
-1 );
)?.toFixed( 2 ),
} )
)
);
} else {
onChange( {
sale_price:
addFixedOrPercentage(
selection.sale_price,
value,
-1
)?.toFixed( 2 ),
} );
}
}, },
} ); } );
onClose(); onClose();
@ -388,19 +326,12 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
date_on_sale_from_gmt:
value,
} ) )
);
} else {
onChange( {
date_on_sale_from_gmt: value, date_on_sale_from_gmt: value,
} ); } ) )
} );
}, },
} ); } );
handlePrompt( { handlePrompt( {
@ -417,18 +348,12 @@ export function PricingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
date_on_sale_to_gmt: value,
} ) )
);
} else {
onChange( {
date_on_sale_to_gmt: value, date_on_sale_to_gmt: value,
} ); } ) )
} );
}, },
} ); } );
onClose(); onClose();

View File

@ -21,9 +21,7 @@ export function SetListPriceMenuItem( {
return ( return (
<MenuItem <MenuItem
onClick={ () => { onClick={ () => {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
recordEvent( 'product_variations_menu_pricing_select', { recordEvent( 'product_variations_menu_pricing_select', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -37,18 +35,12 @@ export function SetListPriceMenuItem( {
action: 'list_price_set', action: 'list_price_set',
variation_id: ids, variation_id: ids,
} ); } );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
regular_price: value,
} ) )
);
} else {
onChange( {
regular_price: value, regular_price: value,
} ); } ) )
} );
}, },
} ); } );
onClose(); onClose();

View File

@ -22,31 +22,20 @@ export function ShippingMenuItem( {
onClose, onClose,
supportsMultipleSelection = false, supportsMultipleSelection = false,
}: VariationActionsMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
function handleDimensionsChange( function handleDimensionsChange(
value: Partial< ProductVariation[ 'dimensions' ] > value: Partial< ProductVariation[ 'dimensions' ] >
) { ) {
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id, dimensions } ) => ( {
selection.map( ( { id, dimensions } ) => ( { id,
id,
dimensions: {
...dimensions,
...value,
},
} ) )
);
} else {
onChange( {
dimensions: { dimensions: {
...selection.dimensions, ...dimensions,
...value, ...value,
}, },
} ); } ) )
} );
} }
return ( return (
@ -87,20 +76,14 @@ export function ShippingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map(
selection.map( ( { id, virtual } ) => ( {
( { id, virtual } ) => ( { id,
id, virtual: ! virtual,
virtual: ! virtual, } )
} ) )
) );
);
} else {
onChange( {
virtual: ! selection.virtual,
} );
}
recordEvent( recordEvent(
'product_variations_menu_shipping_update', 'product_variations_menu_shipping_update',
{ {
@ -225,16 +208,12 @@ export function ShippingMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id, weight: value,
weight: value, } ) )
} ) ) );
);
} else {
onChange( { weight: value } );
}
}, },
} ); } );
onClose(); onClose();

View File

@ -22,9 +22,7 @@ export function ToggleVisibilityMenuItem( {
} }
function handleMenuItemClick() { function handleMenuItemClick() {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
recordEvent( 'product_variations_menu_toggle_visibility_select', { recordEvent( 'product_variations_menu_toggle_visibility_select', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -32,18 +30,12 @@ export function ToggleVisibilityMenuItem( {
variation_id: ids, variation_id: ids,
} ); } );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id, status } ) => ( {
selection.map( ( { id, status } ) => ( { id,
id, status: toggleStatus( status ),
status: toggleStatus( status ), } ) )
} ) ) );
);
} else {
onChange( {
status: toggleStatus( selection.status ),
} );
}
recordEvent( 'product_variations_toggle_visibility_update', { recordEvent( 'product_variations_toggle_visibility_update', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,

View File

@ -1,13 +1,11 @@
/** /**
* External dependencies * External dependencies
*/ */
import { ProductVariation } from '@woocommerce/data'; import { PartialProductVariation, ProductVariation } from '@woocommerce/data';
export type VariationActionsMenuItemProps = { export type VariationActionsMenuItemProps = {
selection: ProductVariation | ProductVariation[]; selection: ProductVariation[];
onChange( onChange( values: PartialProductVariation[] ): void;
variation: Partial< ProductVariation > | Partial< ProductVariation >[]
): void;
onClose(): void; onClose(): void;
supportsMultipleSelection?: boolean; supportsMultipleSelection?: boolean;
}; };

View File

@ -21,9 +21,7 @@ export function UpdateStockMenuItem( {
return ( return (
<MenuItem <MenuItem
onClick={ () => { onClick={ () => {
const ids = Array.isArray( selection ) const ids = selection.map( ( { id } ) => id );
? selection.map( ( { id } ) => id )
: selection.id;
recordEvent( 'product_variations_menu_inventory_select', { recordEvent( 'product_variations_menu_inventory_select', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
@ -44,20 +42,13 @@ export function UpdateStockMenuItem( {
variation_id: ids, variation_id: ids,
} }
); );
if ( Array.isArray( selection ) ) { onChange(
onChange( selection.map( ( { id } ) => ( {
selection.map( ( { id } ) => ( { id,
id,
stock_quantity: stockQuantity,
manage_stock: true,
} ) )
);
} else {
onChange( {
stock_quantity: stockQuantity, stock_quantity: stockQuantity,
manage_stock: true, manage_stock: true,
} ); } ) )
} );
}, },
} ); } );
onClose(); onClose();

View File

@ -3,6 +3,7 @@
*/ */
import { import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
PartialProductVariation,
ProductAttribute, ProductAttribute,
ProductVariation, ProductVariation,
} from '@woocommerce/data'; } from '@woocommerce/data';
@ -251,7 +252,7 @@ export function useVariations( { productId }: UseVariationsProps ) {
async function onUpdate( { async function onUpdate( {
id: variationId, id: variationId,
...variation ...variation
}: Partial< ProductVariation > ) { }: PartialProductVariation ) {
if ( isUpdating[ variationId ] ) return; if ( isUpdating[ variationId ] ) return;
const { updateProductVariation } = dispatch( const { updateProductVariation } = dispatch(
@ -315,7 +316,7 @@ export function useVariations( { productId }: UseVariationsProps ) {
} ); } );
} }
async function onBatchUpdate( values: Partial< ProductVariation >[] ) { async function onBatchUpdate( values: PartialProductVariation[] ) {
// @ts-expect-error There are no types for this. // @ts-expect-error There are no types for this.
const { invalidateResolution: coreInvalidateResolution } = const { invalidateResolution: coreInvalidateResolution } =
dispatch( 'core' ); dispatch( 'core' );
@ -376,7 +377,7 @@ export function useVariations( { productId }: UseVariationsProps ) {
return { update: result }; return { update: result };
} }
async function onBatchDelete( values: Pick< ProductVariation, 'id' >[] ) { async function onBatchDelete( values: PartialProductVariation[] ) {
// @ts-expect-error There are no types for this. // @ts-expect-error There are no types for this.
const { invalidateResolution: coreInvalidateResolution } = const { invalidateResolution: coreInvalidateResolution } =
dispatch( 'core' ); dispatch( 'core' );
@ -409,7 +410,7 @@ export function useVariations( { productId }: UseVariationsProps ) {
} >( } >(
{ product_id: productId }, { product_id: productId },
{ {
delete: subset, delete: subset.map( ( { id } ) => id ),
} }
); );

View File

@ -18,6 +18,10 @@ export function MultipleUpdateMenu( {
onChange, onChange,
onDelete, onDelete,
}: VariationActionsMenuProps ) { }: VariationActionsMenuProps ) {
if ( ! selection ) {
return null;
}
return ( return (
<Dropdown <Dropdown
// @ts-expect-error missing prop in types. // @ts-expect-error missing prop in types.

View File

@ -6,7 +6,6 @@ import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { moreVertical } from '@wordpress/icons'; import { moreVertical } from '@wordpress/icons';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { ProductVariation } from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
@ -20,6 +19,10 @@ export function SingleUpdateMenu( {
onChange, onChange,
onDelete, onDelete,
}: VariationActionsMenuProps ) { }: VariationActionsMenuProps ) {
if ( ! selection || selection.length !== 1 ) {
return null;
}
return ( return (
<DropdownMenu <DropdownMenu
popoverProps={ { popoverProps={ {
@ -32,7 +35,7 @@ export function SingleUpdateMenu( {
onClick() { onClick() {
recordEvent( 'product_variations_menu_view', { recordEvent( 'product_variations_menu_view', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
variation_id: ( selection as ProductVariation ).id, variation_id: selection[ 0 ].id,
} ); } );
}, },
} } } }

View File

@ -23,7 +23,7 @@ const mockVariation = {
downloads: [], downloads: [],
name: '', name: '',
parent_id: 1, parent_id: 1,
} as ProductVariation; } as unknown as ProductVariation;
const anotherMockVariation = { const anotherMockVariation = {
id: 11, id: 11,
@ -32,7 +32,7 @@ const anotherMockVariation = {
downloads: [], downloads: [],
name: '', name: '',
parent_id: 1, parent_id: 1,
} as ProductVariation; } as unknown as ProductVariation;
describe( 'MultipleUpdateMenu', () => { describe( 'MultipleUpdateMenu', () => {
let onChangeMock: jest.Mock, onDeleteMock: jest.Mock; let onChangeMock: jest.Mock, onDeleteMock: jest.Mock;
@ -83,7 +83,7 @@ describe( 'SingleUpdateMenu', () => {
it( 'should trigger product_variations_menu_view track when dropdown toggled', () => { it( 'should trigger product_variations_menu_view track when dropdown toggled', () => {
const { getByRole } = render( const { getByRole } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -101,7 +101,7 @@ describe( 'SingleUpdateMenu', () => {
it( 'should render dropdown with pricing, inventory, and delete options when opened', () => { it( 'should render dropdown with pricing, inventory, and delete options when opened', () => {
const { queryByText, getByRole } = render( const { queryByText, getByRole } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -115,7 +115,7 @@ describe( 'SingleUpdateMenu', () => {
it( 'should call onDelete when Delete menuItem is clicked', async () => { it( 'should call onDelete when Delete menuItem is clicked', async () => {
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -129,7 +129,7 @@ describe( 'SingleUpdateMenu', () => {
it( 'should open Inventory sub-menu if Inventory is clicked with click track', async () => { it( 'should open Inventory sub-menu if Inventory is clicked with click track', async () => {
const { queryByText, getByRole, getByText } = render( const { queryByText, getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -140,7 +140,7 @@ describe( 'SingleUpdateMenu', () => {
'product_variations_menu_inventory_click', 'product_variations_menu_inventory_click',
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( queryByText( 'Update stock' ) ).toBeInTheDocument(); expect( queryByText( 'Update stock' ) ).toBeInTheDocument();
@ -156,7 +156,7 @@ describe( 'SingleUpdateMenu', () => {
window.prompt = jest.fn().mockReturnValue( '10' ); window.prompt = jest.fn().mockReturnValue( '10' );
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -170,19 +170,22 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'stock_quantity_set', action: 'stock_quantity_set',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
stock_quantity: 10, {
manage_stock: true, id: 10,
} ); stock_quantity: 10,
manage_stock: true,
},
] );
expect( recordEvent ).toHaveBeenCalledWith( expect( recordEvent ).toHaveBeenCalledWith(
'product_variations_menu_inventory_update', 'product_variations_menu_inventory_update',
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'stock_quantity_set', action: 'stock_quantity_set',
variation_id: 10, variation_id: [ 10 ],
} }
); );
} ); } );
@ -191,7 +194,7 @@ describe( 'SingleUpdateMenu', () => {
window.prompt = jest.fn().mockReturnValue( null ); window.prompt = jest.fn().mockReturnValue( null );
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -205,7 +208,7 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'stock_quantity_set', action: 'stock_quantity_set',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).not.toHaveBeenCalledWith( { expect( onChangeMock ).not.toHaveBeenCalledWith( {
@ -217,7 +220,7 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'stock_quantity_set', action: 'stock_quantity_set',
variation_id: 10, variation_id: [ 10 ],
} }
); );
} ); } );
@ -225,7 +228,7 @@ describe( 'SingleUpdateMenu', () => {
it( 'should call onChange with toggled manage_stock when toggle "track quantity" is clicked', async () => { it( 'should call onChange with toggled manage_stock when toggle "track quantity" is clicked', async () => {
const { getByRole, getByText, rerender } = render( const { getByRole, getByText, rerender } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -239,16 +242,19 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'manage_stock_toggle', action: 'manage_stock_toggle',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
manage_stock: true, {
} ); id: 10,
manage_stock: true,
},
] );
onChangeMock.mockClear(); onChangeMock.mockClear();
rerender( rerender(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation, manage_stock: true } } selection={ [ { ...mockVariation, manage_stock: true } ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -256,15 +262,18 @@ describe( 'SingleUpdateMenu', () => {
await fireEvent.click( getByRole( 'button', { name: 'Actions' } ) ); await fireEvent.click( getByRole( 'button', { name: 'Actions' } ) );
await fireEvent.click( getByText( 'Inventory' ) ); await fireEvent.click( getByText( 'Inventory' ) );
await fireEvent.click( getByText( 'Toggle "track quantity"' ) ); await fireEvent.click( getByText( 'Toggle "track quantity"' ) );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
manage_stock: false, {
} ); id: 10,
manage_stock: false,
},
] );
} ); } );
it( 'should call onChange with toggled stock_status when toggle "Set status to In stock" is clicked', async () => { it( 'should call onChange with toggled stock_status when toggle "Set status to In stock" is clicked', async () => {
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -278,19 +287,22 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_in_stock', action: 'set_status_in_stock',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
stock_status: PRODUCT_STOCK_STATUS_KEYS.instock, {
manage_stock: false, id: 10,
} ); stock_status: PRODUCT_STOCK_STATUS_KEYS.instock,
manage_stock: false,
},
] );
} ); } );
it( 'should call onChange with toggled stock_status when toggle "Set status to Out of stock" is clicked', async () => { it( 'should call onChange with toggled stock_status when toggle "Set status to Out of stock" is clicked', async () => {
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -304,19 +316,22 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_out_of_stock', action: 'set_status_out_of_stock',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
stock_status: PRODUCT_STOCK_STATUS_KEYS.outofstock, {
manage_stock: false, id: 10,
} ); stock_status: PRODUCT_STOCK_STATUS_KEYS.outofstock,
manage_stock: false,
},
] );
} ); } );
it( 'should call onChange with toggled stock_status when toggle "Set status to On back order" is clicked', async () => { it( 'should call onChange with toggled stock_status when toggle "Set status to On back order" is clicked', async () => {
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -330,20 +345,23 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_on_back_order', action: 'set_status_on_back_order',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
stock_status: PRODUCT_STOCK_STATUS_KEYS.onbackorder, {
manage_stock: false, id: 10,
} ); stock_status: PRODUCT_STOCK_STATUS_KEYS.onbackorder,
manage_stock: false,
},
] );
} ); } );
it( 'should call onChange with low_stock_amount when Edit low stock threshold is clicked', async () => { it( 'should call onChange with low_stock_amount when Edit low stock threshold is clicked', async () => {
window.prompt = jest.fn().mockReturnValue( '7' ); window.prompt = jest.fn().mockReturnValue( '7' );
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<SingleUpdateMenu <SingleUpdateMenu
selection={ { ...mockVariation } } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -357,13 +375,16 @@ describe( 'SingleUpdateMenu', () => {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'low_stock_amount_set', action: 'low_stock_amount_set',
variation_id: 10, variation_id: [ 10 ],
} }
); );
expect( onChangeMock ).toHaveBeenCalledWith( { expect( onChangeMock ).toHaveBeenCalledWith( [
low_stock_amount: 7, {
manage_stock: true, id: 10,
} ); low_stock_amount: 7,
manage_stock: true,
},
] );
} ); } );
} ); } );
} ); } );

View File

@ -26,7 +26,7 @@ const mockVariation = {
downloads: [], downloads: [],
name: '', name: '',
parent_id: 1, parent_id: 1,
} as ProductVariation; } as unknown as ProductVariation;
const anotherMockVariation = { const anotherMockVariation = {
id: 11, id: 11,
@ -35,7 +35,7 @@ const anotherMockVariation = {
downloads: [], downloads: [],
name: '', name: '',
parent_id: 1, parent_id: 1,
} as ProductVariation; } as unknown as ProductVariation;
describe( 'SingleUpdateMenu', () => { describe( 'SingleUpdateMenu', () => {
let onClickMock: jest.Mock, let onClickMock: jest.Mock,
@ -60,7 +60,7 @@ describe( 'SingleUpdateMenu', () => {
My top level item My top level item
</VariationQuickUpdateMenuItem> </VariationQuickUpdateMenuItem>
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -90,7 +90,7 @@ describe( 'SingleUpdateMenu', () => {
My secondary item My secondary item
</VariationQuickUpdateMenuItem> </VariationQuickUpdateMenuItem>
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -120,7 +120,7 @@ describe( 'SingleUpdateMenu', () => {
My tertiary item My tertiary item
</VariationQuickUpdateMenuItem> </VariationQuickUpdateMenuItem>
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />
@ -150,7 +150,7 @@ describe( 'SingleUpdateMenu', () => {
My shipping item My shipping item
</VariationQuickUpdateMenuItem> </VariationQuickUpdateMenuItem>
<SingleUpdateMenu <SingleUpdateMenu
selection={ mockVariation } selection={ [ mockVariation ] }
onChange={ onChangeMock } onChange={ onChangeMock }
onDelete={ onDeleteMock } onDelete={ onDeleteMock }
/> />

View File

@ -1,24 +1,20 @@
/** /**
* External dependencies * External dependencies
*/ */
import { ProductVariation } from '@woocommerce/data'; import { PartialProductVariation, ProductVariation } from '@woocommerce/data';
export type VariationActionsMenuProps = { export type VariationActionsMenuProps = {
disabled?: boolean; disabled?: boolean;
selection: ProductVariation | ProductVariation[]; selection: ProductVariation[];
onChange( variation: Partial< ProductVariation > ): void; onChange( values: PartialProductVariation[] ): void;
onDelete( onDelete( values: PartialProductVariation[] ): void;
variation: ProductVariation | Partial< ProductVariation >[]
): void;
}; };
export type VariationQuickUpdateSlotProps = { export type VariationQuickUpdateSlotProps = {
group: string; group: string;
supportsMultipleSelection: boolean; supportsMultipleSelection: boolean;
selection: ProductVariation | ProductVariation[]; selection: ProductVariation[];
onChange: ( onChange: ( values: PartialProductVariation[] ) => void;
variation: Partial< ProductVariation > | Partial< ProductVariation >[]
) => void;
onClose: () => void; onClose: () => void;
}; };
@ -37,8 +33,6 @@ export type MenuItemProps = {
| 'onClose' | 'onClose'
| 'selection' ]: VariationQuickUpdateSlotProps[ K ]; | 'selection' ]: VariationQuickUpdateSlotProps[ K ];
} ) => void; } ) => void;
onChange?: ( onChange?: ( values: PartialProductVariation[] ) => void;
variation: Partial< ProductVariation > | Partial< ProductVariation >[]
) => void;
onClose?: () => void; onClose?: () => void;
}; };

View File

@ -5,7 +5,6 @@ import { MenuGroup, MenuItem } from '@wordpress/components';
import { createElement, Fragment } from '@wordpress/element'; import { createElement, Fragment } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { ProductVariation } from '@woocommerce/data';
import classNames from 'classnames'; import classNames from 'classnames';
/** /**
@ -32,6 +31,11 @@ export function VariationActions( {
onClose: () => void; onClose: () => void;
supportsMultipleSelection: boolean; supportsMultipleSelection: boolean;
} ) { } ) {
const singleSelection =
! supportsMultipleSelection && selection.length === 1
? selection[ 0 ]
: null;
return ( return (
<div <div
className={ classNames( { className={ classNames( {
@ -45,7 +49,7 @@ export function VariationActions( {
: sprintf( : sprintf(
/** Translators: Variation ID */ /** Translators: Variation ID */
__( 'Variation Id: %s', 'woocommerce' ), __( 'Variation Id: %s', 'woocommerce' ),
( selection as ProductVariation ).id singleSelection?.id
) )
} }
> >
@ -64,14 +68,13 @@ export function VariationActions( {
</> </>
) : ( ) : (
<MenuItem <MenuItem
href={ ( selection as ProductVariation ).permalink } href={ singleSelection?.permalink }
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={ () => { onClick={ () => {
recordEvent( 'product_variations_preview', { recordEvent( 'product_variations_preview', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
variation_id: ( selection as ProductVariation ) variation_id: singleSelection?.id,
.id,
} ); } );
} } } }
> >

View File

@ -2,7 +2,11 @@
* External dependencies * External dependencies
*/ */
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { ProductAttribute, ProductVariation } from '@woocommerce/data'; import {
PartialProductVariation,
ProductAttribute,
ProductVariation,
} from '@woocommerce/data';
export type VariationsTableRowProps = { export type VariationsTableRowProps = {
variation: ProductVariation; variation: ProductVariation;
@ -11,8 +15,8 @@ export type VariationsTableRowProps = {
isSelected?: boolean; isSelected?: boolean;
isSelectionDisabled?: boolean; isSelectionDisabled?: boolean;
hideActionButtons?: boolean; hideActionButtons?: boolean;
onChange( variation: ProductVariation ): void; onChange( variation: PartialProductVariation ): void;
onDelete( variation: ProductVariation ): void; onDelete( variation: PartialProductVariation ): void;
onEdit( event: MouseEvent< HTMLAnchorElement > ): void; onEdit( event: MouseEvent< HTMLAnchorElement > ): void;
onSelect( value: boolean ): void; onSelect( value: boolean ): void;
}; };

View File

@ -4,7 +4,7 @@
import { Tag, __experimentalTooltip as Tooltip } from '@woocommerce/components'; import { Tag, __experimentalTooltip as Tooltip } from '@woocommerce/components';
import { CurrencyContext } from '@woocommerce/currency'; import { CurrencyContext } from '@woocommerce/currency';
import { ProductVariation } from '@woocommerce/data'; import { PartialProductVariation, ProductVariation } from '@woocommerce/data';
import { getNewPath } from '@woocommerce/navigation'; import { getNewPath } from '@woocommerce/navigation';
import { Button, CheckboxControl, Spinner } from '@wordpress/components'; import { Button, CheckboxControl, Spinner } from '@wordpress/components';
import { import {
@ -90,11 +90,12 @@ export function VariationsTableRow( {
[ variableAttributes, variation ] [ variableAttributes, variation ]
); );
function handleChange( value: Partial< ProductVariation > ) { function handleChange( values: PartialProductVariation[] ) {
onChange( { onChange( values[ 0 ] );
...variation, }
...value,
} ); function handleDelete( values: PartialProductVariation[] ) {
onDelete( values[ 0 ] );
} }
return ( return (
@ -241,9 +242,9 @@ export function VariationsTableRow( {
</Button> </Button>
<SingleUpdateMenu <SingleUpdateMenu
selection={ variation } selection={ [ variation ] }
onChange={ handleChange } onChange={ handleChange }
onDelete={ onDelete } onDelete={ handleDelete }
/> />
</> </>
) } ) }

View File

@ -3,7 +3,11 @@
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { Button, CheckboxControl, Notice } from '@wordpress/components'; import { Button, CheckboxControl, Notice } from '@wordpress/components';
import { Product, ProductVariation } from '@woocommerce/data'; import {
PartialProductVariation,
Product,
ProductVariation,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { ListItem, Sortable } from '@woocommerce/components'; import { ListItem, Sortable } from '@woocommerce/components';
import { import {
@ -37,10 +41,8 @@ type VariationsTableProps = {
noticeActions?: { noticeActions?: {
label: string; label: string;
onClick: ( onClick: (
handleUpdateAll: ( values: Partial< ProductVariation >[] ) => void, handleUpdateAll: ( values: PartialProductVariation[] ) => void,
handleDeleteAll: ( handleDeleteAll: ( values: PartialProductVariation[] ) => void
values: Pick< ProductVariation, 'id' >[]
) => void
) => void; ) => void;
className?: string; className?: string;
variant?: string; variant?: string;
@ -174,7 +176,7 @@ export const VariationsTable = forwardRef<
); );
} }
function handleDeleteVariationClick( variation: ProductVariation ) { function handleDeleteVariationClick( variation: PartialProductVariation ) {
onDelete( variation.id ) onDelete( variation.id )
.then( ( response ) => { .then( ( response ) => {
recordEvent( 'product_variations_delete', { recordEvent( 'product_variations_delete', {
@ -194,7 +196,7 @@ export const VariationsTable = forwardRef<
} ); } );
} }
function handleVariationChange( variation: Partial< ProductVariation > ) { function handleVariationChange( variation: PartialProductVariation ) {
onUpdate( variation ) onUpdate( variation )
.then( ( response ) => { .then( ( response ) => {
recordEvent( 'product_variations_change', { recordEvent( 'product_variations_change', {
@ -214,7 +216,7 @@ export const VariationsTable = forwardRef<
} ); } );
} }
function handleUpdateAll( values: Partial< ProductVariation >[] ) { function handleUpdateAll( values: PartialProductVariation[] ) {
const now = Date.now(); const now = Date.now();
onBatchUpdate( values ) onBatchUpdate( values )
@ -235,10 +237,10 @@ export const VariationsTable = forwardRef<
} ); } );
} }
function handleDeleteAll( values: Partial< ProductVariation >[] ) { function handleDeleteAll( values: PartialProductVariation[] ) {
const now = Date.now(); const now = Date.now();
onBatchDelete( values.map( ( variation ) => variation.id ) ) onBatchDelete( values )
.then( ( response: VariationResponseProps ) => { .then( ( response: VariationResponseProps ) => {
recordEvent( 'product_variations_delete_all', { recordEvent( 'product_variations_delete_all', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,