Merge branch 'trunk' into issue-37839

This commit is contained in:
Jason Kytros 2023-09-01 12:17:34 +03:00
commit 4af01af796
36 changed files with 873 additions and 156 deletions

View File

@ -1,3 +1,5 @@
# Adding actions and filters
Like many WordPress plugins, WooCommerce provides a range of actions and filters through which developers can extend and modify the platform. Like many WordPress plugins, WooCommerce provides a range of actions and filters through which developers can extend and modify the platform.
Often, when writing new code or revising existing code, there is a desire to add new hooks—but this should always be done with thoughtfulness and care. This document aims to provide high-level guidance on the matter. Often, when writing new code or revising existing code, there is a desire to add new hooks—but this should always be done with thoughtfulness and care. This document aims to provide high-level guidance on the matter.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add Inventory item to the global Quick Update dropdown

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add Shipping item to the global Quick Update dropdown

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add woocommerce/taxonomy-field block

View File

@ -26,3 +26,4 @@ export { init as initRequirePassword } from './password';
export { init as initVariationItems } from './variation-items'; export { init as initVariationItems } from './variation-items';
export { init as initVariationOptions } from './variation-options'; export { init as initVariationOptions } from './variation-options';
export { init as initNotice } from './notice'; export { init as initNotice } from './notice';
export { init as initTaxonomy } from './taxonomy';

View File

@ -18,3 +18,4 @@
@import 'password/editor.scss'; @import 'password/editor.scss';
@import 'variation-items/editor.scss'; @import 'variation-items/editor.scss';
@import 'variation-options/editor.scss'; @import 'variation-options/editor.scss';
@import 'taxonomy/editor.scss';

View File

@ -0,0 +1,51 @@
# woocommerce/taxonomy-field block
This is a block that displays a taxonomy field, allowing searching, selection, and creation of new items, to be used in a product context.
Please note that to use this block you need to have the custom taxonomy registered in the backend, attached to the products post type and added to the REST API. Here's a snippet that shows how to add an already registered taxonomy to the REST API:
```php
function YOUR_PREFIX_rest_api_prepare_custom1_to_product( $response, $post ) {
$post_id = $post->get_id();
if ( empty( $response->data[ 'custom1' ] ) ) {
$terms = [];
foreach ( wp_get_post_terms( $post_id, 'custom-taxonomy' ) as $term ) {
$terms[] = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
];
}
$response->data[ 'custom1' ] = $terms;
}
return $response;
}
add_filter( 'woocommerce_rest_prepare_product_object', 'YOUR_PREFIX_rest_api_prepare_custom1_to_product', 10, 2 );
function YOUR_PREFIX_rest_api_add_custom1_to_product( $product, $request, $creating = true ) {
$product_id = $product->get_id();
$params = $request->get_params();
$custom1s = isset( $params['custom1'] ) ? $params['custom1'] : array();
if ( ! empty( $custom1s ) ) {
if ( $custom1s[0]['id'] ) {
$custom1s = array_map(
function ( $custom1 ) {
return absint( $custom1['id'] );
},
$custom1s
);
} else {
$custom1s = array_map( 'absint', $custom1s );
}
wp_set_object_terms( $product_id, $custom1s, 'custom-taxonomy' );
}
}
add_filter( 'woocommerce_rest_insert_product_object', 'YOUR_PREFIX_rest_api_add_custom1_to_product', 10, 3 );
```

View File

@ -0,0 +1,38 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/taxonomy-field",
"title": "Taxonomy",
"category": "widgets",
"description": "A block that displays a taxonomy field, allowing searching, selection, and creation of new items",
"keywords": [ "taxonomy"],
"textdomain": "default",
"attributes": {
"slug": {
"type": "string",
"__experimentalRole": "content"
},
"property": {
"type": "string",
"__experimentalRole": "content"
},
"label": {
"type": "string",
"__experimentalRole": "content"
},
"createTitle": {
"type": "string",
"__experimentalRole": "content"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css"
}

View File

@ -0,0 +1,192 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { BaseControl, Button, Modal, TextControl } from '@wordpress/components';
import {
useState,
useEffect,
createElement,
createInterpolateElement,
useCallback,
} from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import {
__experimentalSelectTreeControl as SelectTree,
TreeItemType as Item,
} from '@woocommerce/components';
import { useDebounce, useInstanceId } from '@wordpress/compose';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { Taxonomy } from './types';
import useTaxonomySearch from './use-taxonomy-search';
type CreateTaxonomyModalProps = {
initialName?: string;
hierarchical: boolean;
slug: string;
title: string;
onCancel: () => void;
onCreate: ( taxonomy: Taxonomy ) => void;
};
export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
onCancel,
onCreate,
initialName,
slug,
hierarchical,
title,
} ) => {
const [ categoryParentTypedValue, setCategoryParentTypedValue ] =
useState( '' );
const [ allEntries, setAllEntries ] = useState< Taxonomy[] >( [] );
const { searchEntity, isResolving } = useTaxonomySearch( slug );
const searchDelayed = useDebounce(
useCallback(
( val ) => searchEntity( val || '' ).then( setAllEntries ),
[]
),
150
);
useEffect( () => {
searchDelayed( '' );
}, [] );
const { saveEntityRecord } = useDispatch( 'core' );
const [ isCreating, setIsCreating ] = useState( false );
const [ errorMessage, setErrorMessage ] = useState< string | null >( null );
const [ name, setName ] = useState( initialName || '' );
const [ parent, setParent ] = useState< Taxonomy | null >( null );
const onSave = async () => {
setErrorMessage( null );
try {
const newTaxonomy: Taxonomy = await saveEntityRecord(
'taxonomy',
slug,
{
name,
parent: parent ? parent.id : null,
},
{
throwOnError: true,
}
);
onCreate( newTaxonomy );
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch ( e: any ) {
setIsCreating( false );
if ( e.message ) {
setErrorMessage( e.message );
} else {
setErrorMessage(
__( `Failed to create taxonomy`, 'woocommerce' )
);
throw e;
}
}
};
const id = useInstanceId( BaseControl, 'taxonomy_name' ) as string;
const selectId = useInstanceId(
SelectTree,
'parent-taxonomy-select'
) as string;
return (
<Modal
title={ title }
onRequestClose={ onCancel }
className="woocommerce-create-new-taxonomy-modal"
>
<div className="woocommerce-create-new-taxonomy-modal__wrapper">
<BaseControl
id={ id }
label={ __( 'Name', 'woocommerce' ) }
help={ errorMessage }
className={ classNames( {
'has-error': errorMessage,
} ) }
>
<TextControl
id={ id }
value={ name }
onChange={ setName }
/>
</BaseControl>
{ hierarchical && (
<SelectTree
isLoading={ isResolving }
label={ createInterpolateElement(
__( 'Parent <optional/>', 'woocommerce' ),
{
optional: (
<span className="woocommerce-product-form__optional-input">
{ __( '(optional)', 'woocommerce' ) }
</span>
),
}
) }
id={ selectId }
items={ allEntries.map( ( taxonomy ) => ( {
label: taxonomy.name,
value: String( taxonomy.id ),
parent:
taxonomy.parent > 0
? String( taxonomy.parent )
: undefined,
} ) ) }
shouldNotRecursivelySelect
selected={
parent
? {
value: String( parent.id ),
label: parent.name,
}
: undefined
}
onSelect={ ( item: Item ) =>
item &&
setParent( {
id: +item.value,
name: item.label,
parent: item.parent ? +item.parent : 0,
} )
}
onRemove={ () => setParent( null ) }
onInputChange={ ( value ) => {
searchDelayed( value );
setCategoryParentTypedValue( value || '' );
} }
createValue={ categoryParentTypedValue }
/>
) }
<div className="woocommerce-create-new-taxonomy-modal__buttons">
<Button
isSecondary
onClick={ onCancel }
disabled={ isCreating }
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
isPrimary
disabled={ name.length === 0 || isCreating }
isBusy={ isCreating }
onClick={ onSave }
>
{ __( 'Save', 'woocommerce' ) }
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,184 @@
/**
* External dependencies
*/
import type { BlockAttributes } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import {
createElement,
useState,
Fragment,
useCallback,
useEffect,
} from '@wordpress/element';
import '@woocommerce/settings';
import { __experimentalSelectTreeControl as SelectTreeControl } from '@woocommerce/components';
import { useEntityProp } from '@wordpress/core-data';
import { useDebounce, useInstanceId } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { CreateTaxonomyModal } from './create-taxonomy-modal';
import { Taxonomy, TaxonomyMetadata } from './types';
import useTaxonomySearch from './use-taxonomy-search';
interface TaxonomyBlockAttributes extends BlockAttributes {
label: string;
slug: string;
property: string;
createTitle: string;
}
export function Edit( {
attributes,
}: {
attributes: TaxonomyBlockAttributes;
} ) {
const blockProps = useBlockProps();
const { hierarchical }: TaxonomyMetadata = useSelect(
( select ) =>
select( 'core' ).getTaxonomy( attributes.slug ) || {
hierarchical: false,
}
);
const { label, slug, property, createTitle } = attributes;
const [ searchValue, setSearchValue ] = useState( '' );
const [ allEntries, setAllEntries ] = useState< Taxonomy[] >( [] );
const { searchEntity, isResolving } = useTaxonomySearch( slug, {
fetchParents: hierarchical,
} );
const searchDelayed = useDebounce(
useCallback(
( val ) => {
setSearchValue( val );
searchEntity( val || '' ).then( setAllEntries );
},
[ hierarchical ]
),
150
);
useEffect( () => {
searchDelayed( '' );
}, [] );
const [ selectedEntries, setSelectedEntries ] = useEntityProp< Taxonomy[] >(
'postType',
'product',
property
);
const mappedEntries = selectedEntries.map( ( b ) => ( {
value: String( b.id ),
label: b.name,
} ) );
const [ showCreateNewModal, setShowCreateNewModal ] = useState( false );
const mappedAllEntries = allEntries.map( ( taxonomy ) => ( {
parent:
hierarchical && taxonomy.parent && taxonomy.parent > 0
? String( taxonomy.parent )
: undefined,
label: taxonomy.name,
value: String( taxonomy.id ),
} ) );
return (
<div { ...blockProps }>
<>
<SelectTreeControl
id={
useInstanceId(
SelectTreeControl,
'woocommerce-taxonomy-select'
) as string
}
label={ label }
isLoading={ isResolving }
multiple
createValue={ searchValue }
onInputChange={ searchDelayed }
shouldNotRecursivelySelect
shouldShowCreateButton={ ( typedValue ) =>
! typedValue ||
mappedAllEntries.findIndex(
( taxonomy ) =>
taxonomy.label.toLowerCase() ===
typedValue.toLowerCase()
) === -1
}
onCreateNew={ () => setShowCreateNewModal( true ) }
items={ mappedAllEntries }
selected={ mappedEntries }
onSelect={ ( selectedItems ) => {
if ( Array.isArray( selectedItems ) ) {
setSelectedEntries( [
...selectedItems.map( ( i ) => ( {
id: +i.value,
name: i.label,
parent: +( i.parent || 0 ),
} ) ),
...selectedEntries,
] );
} else {
setSelectedEntries( [
{
id: +selectedItems.value,
name: selectedItems.label,
parent: +( selectedItems.parent || 0 ),
},
...selectedEntries,
] );
}
} }
onRemove={ ( removedItems ) => {
if ( Array.isArray( removedItems ) ) {
setSelectedEntries(
selectedEntries.filter(
( taxonomy ) =>
! removedItems.find(
( item ) =>
item.value ===
String( taxonomy.id )
)
)
);
} else {
setSelectedEntries(
selectedEntries.filter(
( taxonomy ) =>
String( taxonomy.id ) !==
removedItems.value
)
);
}
} }
></SelectTreeControl>
{ showCreateNewModal && (
<CreateTaxonomyModal
slug={ slug }
hierarchical={ hierarchical }
title={ createTitle }
onCancel={ () => setShowCreateNewModal( false ) }
onCreate={ ( taxonomy ) => {
setShowCreateNewModal( false );
setSearchValue( '' );
setSelectedEntries( [
{
id: taxonomy.id,
name: taxonomy.name,
parent: taxonomy.parent,
},
...selectedEntries,
] );
} }
initialName={ searchValue }
/>
) }
</>
</div>
);
}

View File

@ -0,0 +1,24 @@
.components-modal__screen-overlay {
.woocommerce-create-new-taxonomy-modal {
min-width: 650px;
overflow: visible;
&__buttons {
margin-top: $gap-larger;
display: flex;
flex-direction: row;
gap: $gap-smaller;
justify-content: flex-end;
}
}
.has-error {
.components-text-control__input {
border-color: $studio-red-50;
}
.components-base-control__help {
color: $studio-red-50;
}
}
}

View File

@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { initBlock } from '../../utils';
import metadata from './block.json';
import { Edit } from './edit';
const { name } = metadata;
export { metadata, name };
export const settings = {
example: {},
edit: Edit,
};
export const init = () =>
initBlock( {
name,
metadata: metadata as never,
settings,
} );

View File

@ -0,0 +1,9 @@
export interface Taxonomy {
id: number;
name: string;
parent: number;
}
export interface TaxonomyMetadata {
hierarchical: boolean;
}

View File

@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { resolveSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { Taxonomy } from './types';
async function getTaxonomiesMissingParents(
taxonomies: Taxonomy[],
taxonomyName: string
): Promise< Taxonomy[] > {
// Retrieve the missing parent objects incase not all of them were included.
const missingParentIds: number[] = [];
const taxonomiesLookup: Record< number, Taxonomy > = {};
taxonomies.forEach( ( taxonomy ) => {
taxonomiesLookup[ taxonomy.id ] = taxonomy;
} );
taxonomies.forEach( ( taxonomy ) => {
if ( taxonomy.parent > 0 && ! taxonomiesLookup[ taxonomy.parent ] ) {
missingParentIds.push( taxonomy.parent );
}
} );
if ( missingParentIds.length > 0 ) {
return resolveSelect( 'core' )
.getEntityRecords< Taxonomy[] >( 'taxonomy', taxonomyName, {
include: missingParentIds,
} )
.then( ( parentTaxonomies ) => {
return getTaxonomiesMissingParents(
[ ...( parentTaxonomies as Taxonomy[] ), ...taxonomies ],
taxonomyName
);
} );
}
return taxonomies;
}
const PAGINATION_SIZE = 30;
interface UseTaxonomySearchOptions {
fetchParents?: boolean;
}
const useTaxonomySearch = (
taxonomyName: string,
options: UseTaxonomySearchOptions = { fetchParents: true }
): {
searchEntity: ( search: string ) => Promise< Taxonomy[] >;
isResolving: boolean;
} => {
const [ isSearching, setIsSearching ] = useState( false );
async function searchEntity( search: string ): Promise< Taxonomy[] > {
setIsSearching( true );
let taxonomies: Taxonomy[] = [];
try {
taxonomies = await resolveSelect( 'core' ).getEntityRecords<
Taxonomy[]
>( 'taxonomy', taxonomyName, {
per_page: PAGINATION_SIZE,
search,
} );
if ( options?.fetchParents ) {
taxonomies = await getTaxonomiesMissingParents(
taxonomies,
taxonomyName
);
}
} catch ( e ) {
setIsSearching( false );
}
return taxonomies;
}
return {
searchEntity,
isResolving: isSearching,
};
};
export default useTaxonomySearch;

View File

@ -2,7 +2,6 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { ProductVariation } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { Dropdown, MenuGroup, MenuItem } from '@wordpress/components'; import { Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
@ -14,23 +13,18 @@ import { chevronRight } from '@wordpress/icons';
import { TRACKS_SOURCE } from '../../../constants'; import { TRACKS_SOURCE } from '../../../constants';
import { PRODUCT_STOCK_STATUS_KEYS } from '../../../utils/get-product-stock-status'; import { PRODUCT_STOCK_STATUS_KEYS } from '../../../utils/get-product-stock-status';
import { UpdateStockMenuItem } from '../update-stock-menu-item'; import { UpdateStockMenuItem } from '../update-stock-menu-item';
import { VariationActionsMenuItemProps } from '../types';
export type InventoryMenuItemProps = { import { handlePrompt } from '../../../utils/handle-prompt';
variation: ProductVariation;
handlePrompt(
label?: string,
parser?: ( value: string ) => Partial< ProductVariation > | null
): void;
onChange( values: Partial< ProductVariation > ): void;
onClose(): void;
};
export function InventoryMenuItem( { export function InventoryMenuItem( {
variation, selection,
handlePrompt,
onChange, onChange,
onClose, onClose,
}: InventoryMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection )
? selection.map( ( { id } ) => id )
: selection.id;
return ( return (
<Dropdown <Dropdown
position="middle right" position="middle right"
@ -41,7 +35,7 @@ export function InventoryMenuItem( {
'product_variations_menu_inventory_click', 'product_variations_menu_inventory_click',
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
variation_id: variation.id, variation_id: ids,
} }
); );
onToggle(); onToggle();
@ -57,7 +51,7 @@ export function InventoryMenuItem( {
<div className="components-dropdown-menu__menu"> <div className="components-dropdown-menu__menu">
<MenuGroup> <MenuGroup>
<UpdateStockMenuItem <UpdateStockMenuItem
selection={ variation } selection={ selection }
onChange={ onChange } onChange={ onChange }
onClose={ onClose } onClose={ onClose }
/> />
@ -68,12 +62,23 @@ export function InventoryMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'manage_stock_toggle', action: 'manage_stock_toggle',
variation_id: variation.id, variation_id: ids,
} }
); );
onChange( { if ( Array.isArray( selection ) ) {
manage_stock: ! variation.manage_stock, onChange(
} ); selection.map(
( { id, manage_stock } ) => ( {
id,
manage_stock: ! manage_stock,
} )
)
);
} else {
onChange( {
manage_stock: ! selection.manage_stock,
} );
}
onClose(); onClose();
} } } }
> >
@ -86,14 +91,25 @@ export function InventoryMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_in_stock', action: 'set_status_in_stock',
variation_id: variation.id, variation_id: ids,
} }
); );
onChange( { if ( Array.isArray( selection ) ) {
stock_status: onChange(
PRODUCT_STOCK_STATUS_KEYS.instock, selection.map( ( { id } ) => ( {
manage_stock: false, id,
} ); stock_status:
PRODUCT_STOCK_STATUS_KEYS.instock,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status:
PRODUCT_STOCK_STATUS_KEYS.instock,
manage_stock: false,
} );
}
onClose(); onClose();
} } } }
> >
@ -106,14 +122,25 @@ export function InventoryMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_out_of_stock', action: 'set_status_out_of_stock',
variation_id: variation.id, variation_id: ids,
} }
); );
onChange( { if ( Array.isArray( selection ) ) {
stock_status: onChange(
PRODUCT_STOCK_STATUS_KEYS.outofstock, selection.map( ( { id } ) => ( {
manage_stock: false, id,
} ); stock_status:
PRODUCT_STOCK_STATUS_KEYS.outofstock,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status:
PRODUCT_STOCK_STATUS_KEYS.outofstock,
manage_stock: false,
} );
}
onClose(); onClose();
} } } }
> >
@ -129,14 +156,25 @@ export function InventoryMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'set_status_on_back_order', action: 'set_status_on_back_order',
variation_id: variation.id, variation_id: ids,
} }
); );
onChange( { if ( Array.isArray( selection ) ) {
stock_status: onChange(
PRODUCT_STOCK_STATUS_KEYS.onbackorder, selection.map( ( { id } ) => ( {
manage_stock: false, id,
} ); stock_status:
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
manage_stock: false,
} ) )
);
} else {
onChange( {
stock_status:
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
manage_stock: false,
} );
}
onClose(); onClose();
} } } }
> >
@ -152,26 +190,40 @@ export function InventoryMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'low_stock_amount_set', action: 'low_stock_amount_set',
variation_id: variation.id, variation_id: ids,
} }
); );
handlePrompt( undefined, ( value ) => { handlePrompt( {
recordEvent( onOk( value ) {
'product_variations_menu_inventory_select', recordEvent(
{ 'product_variations_menu_inventory_update',
source: TRACKS_SOURCE, {
action: 'low_stock_amount_set', source: TRACKS_SOURCE,
variation_id: variation.id, action: 'low_stock_amount_set',
variation_id: ids,
}
);
const lowStockAmount = Number( value );
if ( Number.isNaN( lowStockAmount ) ) {
return null;
} }
); if ( Array.isArray( selection ) ) {
const lowStockAmount = Number( value ); onChange(
if ( Number.isNaN( lowStockAmount ) ) { selection.map( ( { id } ) => ( {
return null; id,
} low_stock_amount:
return { lowStockAmount,
low_stock_amount: lowStockAmount, manage_stock: true,
manage_stock: true, } ) )
}; );
} else {
onChange( {
low_stock_amount:
lowStockAmount,
manage_stock: true,
} );
}
},
} ); } );
onClose(); onClose();
} } } }

View File

@ -5,19 +5,48 @@ import { Dropdown, MenuItem } from '@wordpress/components';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { chevronRight } from '@wordpress/icons'; import { chevronRight } from '@wordpress/icons';
import { ProductVariation } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { ShippingMenuItemProps } from './types';
import { TRACKS_SOURCE } from '../../../constants'; import { TRACKS_SOURCE } from '../../../constants';
import { VariationActionsMenuItemProps } from '../types';
import { handlePrompt } from '../../../utils/handle-prompt';
export function ShippingMenuItem( { export function ShippingMenuItem( {
variation, selection,
handlePrompt, onChange,
onClose, onClose,
}: ShippingMenuItemProps ) { }: VariationActionsMenuItemProps ) {
const ids = Array.isArray( selection )
? selection.map( ( { id } ) => id )
: selection.id;
function handleDimensionsChange(
value: Partial< ProductVariation[ 'dimensions' ] >
) {
if ( Array.isArray( selection ) ) {
onChange(
selection.map( ( { id, dimensions } ) => ( {
id,
dimensions: {
...dimensions,
...value,
},
} ) )
);
} else {
onChange( {
dimensions: {
...selection.dimensions,
...value,
},
} );
}
}
return ( return (
<Dropdown <Dropdown
position="middle right" position="middle right"
@ -26,7 +55,7 @@ export function ShippingMenuItem( {
onClick={ () => { onClick={ () => {
recordEvent( 'product_variations_menu_shipping_click', { recordEvent( 'product_variations_menu_shipping_click', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
variation_id: variation.id, variation_id: ids,
} ); } );
onToggle(); onToggle();
} } } }
@ -46,24 +75,23 @@ export function ShippingMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'dimensions_length_set', action: 'dimensions_length_set',
variation_id: variation.id, variation_id: ids,
} }
); );
handlePrompt( undefined, ( value ) => { handlePrompt( {
recordEvent( onOk( value ) {
'product_variations_menu_shipping_update', recordEvent(
{ 'product_variations_menu_shipping_update',
source: TRACKS_SOURCE, {
action: 'dimensions_length_set', source: TRACKS_SOURCE,
variation_id: variation.id, action: 'dimensions_length_set',
} variation_id: ids,
); }
return { );
dimensions: { handleDimensionsChange( {
...variation.dimensions,
length: value, length: value,
}, } );
}; },
} ); } );
onClose(); onClose();
} } } }
@ -77,24 +105,23 @@ export function ShippingMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'dimensions_width_set', action: 'dimensions_width_set',
variation_id: variation.id, variation_id: ids,
} }
); );
handlePrompt( undefined, ( value ) => { handlePrompt( {
recordEvent( onOk( value ) {
'product_variations_menu_shipping_update', recordEvent(
{ 'product_variations_menu_shipping_update',
source: TRACKS_SOURCE, {
action: 'dimensions_width_set', source: TRACKS_SOURCE,
variation_id: variation.id, action: 'dimensions_width_set',
} variation_id: ids,
); }
return { );
dimensions: { handleDimensionsChange( {
...variation.dimensions,
width: value, width: value,
}, } );
}; },
} ); } );
onClose(); onClose();
} } } }
@ -108,24 +135,23 @@ export function ShippingMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'dimensions_height_set', action: 'dimensions_height_set',
variation_id: variation.id, variation_id: ids,
} }
); );
handlePrompt( undefined, ( value ) => { handlePrompt( {
recordEvent( onOk( value ) {
'product_variations_menu_shipping_update', recordEvent(
{ 'product_variations_menu_shipping_update',
source: TRACKS_SOURCE, {
action: 'dimensions_height_set', source: TRACKS_SOURCE,
variation_id: variation.id, action: 'dimensions_height_set',
} variation_id: ids,
); }
return { );
dimensions: { handleDimensionsChange( {
...variation.dimensions,
height: value, height: value,
}, } );
}; },
} ); } );
onClose(); onClose();
} } } }
@ -139,19 +165,30 @@ export function ShippingMenuItem( {
{ {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
action: 'weight_set', action: 'weight_set',
variation_id: variation.id, variation_id: ids,
} }
); );
handlePrompt( undefined, ( value ) => { handlePrompt( {
recordEvent( onOk( value ) {
'product_variations_menu_shipping_update', recordEvent(
{ 'product_variations_menu_shipping_update',
source: TRACKS_SOURCE, {
action: 'weight_set', source: TRACKS_SOURCE,
variation_id: variation.id, action: 'weight_set',
variation_id: ids,
}
);
if ( Array.isArray( selection ) ) {
onChange(
selection.map( ( { id } ) => ( {
id,
weight: value,
} ) )
);
} else {
onChange( { weight: value } );
} }
); },
return { weight: value };
} ); } );
onClose(); onClose();
} } } }

View File

@ -5,7 +5,6 @@ import { DropdownMenu, 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 { moreVertical } from '@wordpress/icons'; import { moreVertical } from '@wordpress/icons';
import { ProductVariation } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
/** /**
@ -22,21 +21,6 @@ export function VariationActionsMenu( {
onChange, onChange,
onDelete, onDelete,
}: VariationActionsMenuProps ) { }: VariationActionsMenuProps ) {
function handlePrompt(
label: string = __( 'Enter a value', 'woocommerce' ),
parser: ( value: string ) => Partial< ProductVariation > | null = () =>
null
) {
// eslint-disable-next-line no-alert
const value = window.prompt( label );
if ( value === null ) return;
const updates = parser( value.trim() );
if ( updates ) {
onChange( updates );
}
}
return ( return (
<DropdownMenu <DropdownMenu
icon={ moreVertical } icon={ moreVertical }
@ -78,14 +62,13 @@ export function VariationActionsMenu( {
onClose={ onClose } onClose={ onClose }
/> />
<InventoryMenuItem <InventoryMenuItem
variation={ selection } selection={ selection }
handlePrompt={ handlePrompt }
onChange={ onChange } onChange={ onChange }
onClose={ onClose } onClose={ onClose }
/> />
<ShippingMenuItem <ShippingMenuItem
variation={ selection } selection={ selection }
handlePrompt={ handlePrompt } onChange={ onChange }
onClose={ onClose } onClose={ onClose }
/> />
</MenuGroup> </MenuGroup>

View File

@ -13,6 +13,8 @@ import { VariationsActionsMenuProps } from './types';
import { UpdateStockMenuItem } from '../update-stock-menu-item'; import { UpdateStockMenuItem } from '../update-stock-menu-item';
import { PricingMenuItem } from '../pricing-menu-item'; import { PricingMenuItem } from '../pricing-menu-item';
import { SetListPriceMenuItem } from '../set-list-price-menu-item'; import { SetListPriceMenuItem } from '../set-list-price-menu-item';
import { InventoryMenuItem } from '../inventory-menu-item';
import { ShippingMenuItem } from '../shipping-menu-item';
export function VariationsActionsMenu( { export function VariationsActionsMenu( {
selection, selection,
@ -22,7 +24,7 @@ export function VariationsActionsMenu( {
}: VariationsActionsMenuProps ) { }: VariationsActionsMenuProps ) {
return ( return (
<Dropdown <Dropdown
position="bottom right" position="bottom left"
renderToggle={ ( { isOpen, onToggle } ) => ( renderToggle={ ( { isOpen, onToggle } ) => (
<Button <Button
disabled={ disabled } disabled={ disabled }
@ -55,6 +57,16 @@ export function VariationsActionsMenu( {
onChange={ onChange } onChange={ onChange }
onClose={ onClose } onClose={ onClose }
/> />
<InventoryMenuItem
selection={ selection }
onChange={ onChange }
onClose={ onClose }
/>
<ShippingMenuItem
selection={ selection }
onChange={ onChange }
onClose={ onClose }
/>
</MenuGroup> </MenuGroup>
<MenuGroup> <MenuGroup>
<MenuItem <MenuItem

View File

@ -1,27 +1,26 @@
Data # Data
====
WooCommerce Admin data stores implement the [`SqlQuery` class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Reports/SqlQuery.php). WooCommerce Admin data stores implement the [`SqlQuery` class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Reports/SqlQuery.php).
### SqlQuery Class ## SqlQuery Class
The `SqlQuery` class is a SQL Query statement object. Its properties consist of The `SqlQuery` class is a SQL Query statement object. Its properties consist of
- A `context` string identifying the context of the query. - A `context` string identifying the context of the query.
- SQL clause (`type`) string arrays used to construct the SQL statement: - SQL clause (`type`) string arrays used to construct the SQL statement:
- `select` - `select`
- `from` - `from`
- `right_join` - `right_join`
- `join` - `join`
- `left_join` - `left_join`
- `where` - `where`
- `where_time` - `where_time`
- `group_by` - `group_by`
- `having` - `having`
- `order_by` - `order_by`
- `limit` - `limit`
### Reports Data Stores ## Reports Data Stores
The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. The implementation data store classes use the following `SqlQuery` instances: The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. The implementation data store classes use the following `SqlQuery` instances:
@ -49,7 +48,7 @@ Query contexts are named as follows:
- Interval Query = Class Context + `_interval` - Interval Query = Class Context + `_interval`
- Total Query = Class Context + `_total` - Total Query = Class Context + `_total`
### Filters ## Filters
When getting the full statement the clause arrays are passed through two filters where `$context` is the query object context and `$type` is: When getting the full statement the clause arrays are passed through two filters where `$context` is the query object context and `$type` is:
@ -64,13 +63,13 @@ When getting the full statement the clause arrays are passed through two filters
The filters are: The filters are:
- `apply_filters( "wc_admin_clauses_{$type}", $clauses, $context );` - `apply_filters( "woocommerce_analytics_clauses_{$type}", $clauses, $context );`
- `apply_filters( "wc_admin_clauses_{$type}_{$context}", $clauses );` - `apply_filters( "woocommerce_analytics_clauses_{$type}_{$context}", $clauses );`
Example usage Example usage
``` ```php
add_filter( 'wc_admin_clauses_product_stats_select_total', 'my_custom_product_stats' ); add_filter( 'woocommerce_analytics_clauses_product_stats_select_total', 'my_custom_product_stats' );
/** /**
* Add sample data to product stats totals. * Add sample data to product stats totals.
* *

View File

@ -217,6 +217,12 @@ const webpackConfig = {
return null; return null;
} }
if ( request === '@wordpress/router' ) {
// The external wp.router does not exist in WP 6.2 and below, so we need to skip requesting to external here.
// We use the router in the customize store. We can remove this once our minimum support is WP 6.3.
return null;
}
if ( request.startsWith( '@wordpress/edit-site' ) ) { if ( request.startsWith( '@wordpress/edit-site' ) ) {
// The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite. // The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite.
// We use the edit-site components in the customize store. // We use the edit-site components in the customize store.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
FIx WC Admin pages are empty for WP 6.2 and below.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
update the SqlQuery filter prefix in data.md