[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 './products/types';
export type {
PartialProductVariation,
ProductVariation,
ProductVariationAttribute,
ProductVariationImage,

View File

@ -57,25 +57,29 @@ export interface ProductVariationImage {
export type ProductVariation = Omit<
Product,
'slug' | 'attributes' | 'images' | 'manage_stock'
> & {
attributes: ProductVariationAttribute[];
/**
* Variation image data.
*/
image?: ProductVariationImage;
/**
* Stock management at variation level. It can have a
* 'parent' value if the parent product is managing
* the stock at the time the variation was created.
*
* @default false
*/
manage_stock: boolean | 'parent';
/**
* The product id this variation belongs to
*/
parent_id: number;
};
> &
Pick< Product, 'id' > & {
attributes: ProductVariationAttribute[];
/**
* Variation image data.
*/
image?: ProductVariationImage;
/**
* Stock management at variation level. It can have a
* 'parent' value if the parent product is managing
* the stock at the time the variation was created.
*
* @default false
*/
manage_stock: boolean | 'parent';
/**
* The product id this variation belongs to
*/
parent_id: number;
};
export type PartialProductVariation = Partial< ProductVariation > &
Pick< ProductVariation, 'id' >;
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 {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
PartialProductVariation,
Product,
ProductVariation,
useUserPreferences,
} from '@woocommerce/data';
import { useWooBlockProps } from '@woocommerce/block-templates';
@ -115,14 +115,14 @@ export function Edit( {
);
function onSetPrices(
handleUpdateAll: ( update: Partial< ProductVariation >[] ) => void
handleUpdateAll: ( update: PartialProductVariation[] ) => void
) {
recordEvent( 'product_variations_set_prices_select', {
source: TRACKS_SOURCE,
} );
const productVariationsListPromise = resolveSelect(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
).getProductVariations< Pick< ProductVariation, 'id' >[] >( {
).getProductVariations< PartialProductVariation[] >( {
product_id: productId,
order: 'asc',
orderby: 'menu_order',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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